@pmelab/gtd 27.7.1 → 27.9.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 +10 -5
- package/dist/chunks/{wire-C12dahrH.mjs → wire-D3Rc5r22.mjs} +14 -1
- package/dist/flows.mjs +16 -1
- package/dist/gtd.bundle.mjs +203 -32
- package/dist/testkit.mjs +172 -33
- package/dist/wire.mjs +2 -2
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +39 -3
- package/src/cli/Cli.ts +23 -2
- package/src/cli/Program.test.ts +3 -1
- package/src/cli/Program.ts +22 -0
- package/src/edge/Edge.test.ts +217 -34
- package/src/edge/Edge.ts +131 -39
- package/src/edge/index.ts +1 -0
- package/src/flows/runtime.test.ts +27 -0
- package/src/flows/runtime.ts +18 -0
- package/src/replay/Replay.test.ts +169 -0
- package/src/replay/Replay.ts +42 -2
- package/src/replay/Slices.test.ts +73 -0
- package/src/replay/Slices.ts +45 -0
- package/src/replay/Trailers.test.ts +11 -0
- package/src/replay/Trailers.ts +3 -1
- package/src/replay/index.ts +1 -0
- package/src/wire/Slices.ts +25 -0
- package/src/wire/WireSchema.test.ts +18 -0
- package/src/wire/golden/1/slices.json +11 -0
- package/src/wire/index.ts +1 -0
package/dist/testkit.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { checkScript, configProblems, configure, installContext, isFactoryWorkflow, moveScript, protocolMismatch, removeScript, restoreScript, revertScript, showScript, t as flows_exports } from "./flows.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { O as ACCESS_REFUSAL, a as selectPath, d as renderLandPlain, g as statusOf, h as renderBeatJson, i as resolveSession, j as renderFormat, l as noopText, m as beatDocument, n as slicesDocument, o as judgeJson, p as renderBeatPlain, r as globMatches, s as landFields, t as renderSlicesJson, u as renderLandJson, v as beatKindOf, y as demandOf } from "./chunks/wire-D3Rc5r22.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, w as formatFinding } from "./chunks/steering-DVILanVf.mjs";
|
|
4
4
|
import { $ as logWarning, $i as NodeInspectSymbol, $n as map$5, $r as make$19, $t as provide$1, A as catchIf, Ai as add, An as scopeMake, Ar as flatMap$3, At as suspend$5, B as flatMap$4, Bi as getOrElse, Bn as get$3, Br as sync$1, Bt as unsafeMakeLatch, C as all$1, Ci as toArray, Cn as isInterrupted, Cr as exitFail, Ct as sandbox, D as async, Di as GenericTag, Dn as squash, Dr as exitZipRight, Dt as scopedWith$1, E as asVoid$1, Ei as unsafeHead, En as reduceWithContext, Er as exitVoid, Et as scoped$4, F as ensuring$2, Fi as fromIterable, Fn as join$3, Fr as map$3, Ft as tryMap, G as forkIn, Gi as none, Gn as TypeId$10, Gr as whenEffect, Gt as zipLeft, H as fnUntraced, Hi as isSome, Hn as modify$1, Hr as uninterruptible, Ht as when, I as exit, Ii as splitAt, In as sequential, Ir as onInterrupt, It as tryPromise, J as ignore, Ji as left, Jn as all, Jr as DeferredTypeId$1, Jt as spaced$1, K as forkScoped, Ki as some, Kn as Class, Kr as withFiberRuntime, Kt as zipRight$3, L as fail$5, Li as filter$1, Ln as bind, Lr as scopeClose, Lt as try_, M as die$2, Mi as getOption, Mn as _await$2, Mr as interruptAsFiber, Mt as tap, N as dieMessage, Ni as omit, Nn as inheritAll$1, Nr as interruptFiber, Nt as tapErrorCause, O as catchAll$1, Oi as Reference, On as ensuring$3, Or as fiberId, Ot as serviceOption, P as either, Pi as empty$5, Pn as interruptAllAs$1, Pr as interruptible$1, Pt as timeout, Q as isEffect, Qi as BaseProto, Qn as isFailure, Qr as ids, Qt as mergeAll$2, R as failCause$4, Ri as flatMap$5, Rn as bindTo, Rr as succeed$4, Rt as uninterruptible$1, S as addFinalizer$1, Si as takeRight, Sn as isDieType, Sr as exitAs, St as runtime, T as as, Ti as unsafeGet, Tn as map$6, Tr as exitSucceed, Tt as scopeWith, U as forEach$2, Ui as map$8, Un as set$1, Ur as uninterruptibleMask$1, Ut as withFiberRuntime$1, V as flatten$4, Vi as isNone, Vn as make$16, Vr as tap$1, Vt as unsafeMakeSemaphore$1, W as forkDaemon, Wi as match$4, Wn as update$1, Wr as void_$2, Wt as zip$1, X as interruptible, Xi as right, Xn as fail$4, Xr as combine, Xt as tagged, Y as interrupt$4, Yi as match$2, Yn as die, Yr as empty$6, Yt as TaggedError$1, Z as intoDeferred, Zi as pipeArguments, Zn as failCause$3, Zr as combineAll, Zt as fromEffect$5, _ as decodeSync, _i as of, _n as fail$3, _r as deferredIsDone, _t as raceWith, a as Literal, aa as constTrue, ai as size$2, an as updateContext$1, ar as zipRight$2, at as matchCause, b as acquireRelease, bi as reverse, bn as interrupt$3, br as deferredUnsafeDone, bt as runPromise, c as String$, ca as dual, ci as append$1, cn as Scope, cr as asyncInterrupt, ct as negate, d as Union, di as empty$4, dn as close, dr as deferredAwait, dt as orElse, ea as format, ei as none$1, en as provideMerge$1, er as mapBoth, et as makeSemaphore, f as between, fi as filter, fn as extend, fr as deferredComplete, ft as orElseSucceed, gi as map$7, gn as die$1, gr as deferredInterruptWith, gt as race, hi as isEmpty$2, hn as NoSuchElementException, hr as deferredInterrupt, ht as provideService, i as Defect, ia as constFalse, ii as has, in as unsafeRunPromiseExit, ir as zip$2, it as match$5, j as catchTag, ji as get$5, jn as scopeTag, jr as interrupt$2, jt as sync$2, k as catchAllCause, ki as Tag, kn as forEachConcurrentDiscard, kr as fiberRefLocally, kt as succeed$6, l as Struct, la as identity, li as appendAll, ln as addFinalizer, lr as catchAll, lt as onError, m as optional$1, mi as isChunk, mn as make$18, mr as deferredFailCause, mt as provide$2, n as Boolean$, na as equals, ni as make$17, nn as succeed$3, nr as succeed$5, nt as mapBoth$1, o as Never, oa as constUndefined, oi as decode, on as forkIn$1, or as as$1, ot as matchCauseEffect, p as decodeUnknown, pi as fromIterable$1, pn as fork, pr as deferredFail, pt as promise, q as gen, qi as isLeft, qn as spanTag, qr as zipRight$1, qt as zipWith, r as Cause, ra as hasProperty, ri as set$2, rn as unsafeFork, rr as void_$3, rt as mapError, s as Number$, sa as constVoid, si as isFinite, sn as unsafeMakeSemaphore, sr as asVoid, st as matchEffect, t as Array$, ta as toJSON, ti as get$4, tn as scoped$3, tr as match$3, tt as map$4, u as TaggedError, ua as pipe, ui as drop, un as addFinalizerExit, ur as currentContext$1, ut as orDie, v as encodeSync, vi as prepend, vn as failureOrCause, vr as deferredMake, vt as repeat, w as annotateLogs, wi as unsafeFromArray, wn as isInterruptedOnly, wr as exitFailCause, wt as scope, x as acquireUseRelease$2, xi as take$2, xn as interruptors, xr as deferredUnsafeMake, xt as runPromiseExit$1, y as _void, yi as reduce, yn as flipCauseOption, yr as deferredSucceed, yt as retry, z as fiberIdWith, zi as fromNullable, zn as sleep, zr as suspend$4, zt as uninterruptibleMask } from "./chunks/Schema-DO5ttJaI.mjs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
@@ -6817,6 +6817,32 @@ const refNameFault = (name) => {
|
|
|
6817
6817
|
}
|
|
6818
6818
|
};
|
|
6819
6819
|
//#endregion
|
|
6820
|
+
//#region src/replay/Slices.ts
|
|
6821
|
+
const IMPLICIT_SLICE = "main";
|
|
6822
|
+
/** Same shape as mark()'s name rule; kept local so @pmelab/gtd/flows publishes no regex. */
|
|
6823
|
+
const SLICE_NAME = /^[a-z][a-z0-9-]*$/;
|
|
6824
|
+
/**
|
|
6825
|
+
* Folds boundaries into ranges: each kept span's base is the previous kept
|
|
6826
|
+
* span's `leftAt`, so a gap before a span belongs to it, and the last span
|
|
6827
|
+
* runs to the episode tip. A closed span that landed nothing is dropped.
|
|
6828
|
+
*/
|
|
6829
|
+
const sliceRanges = (spans, episode) => {
|
|
6830
|
+
if (spans.some((s) => s.enteredAt === "" || s.leftAt === "")) throw new Error("sliceRanges: a pending turn's \"\" hash is not a commit");
|
|
6831
|
+
const kept = spans.filter((s) => s.leftAt === void 0 || s.enteredAt !== s.leftAt);
|
|
6832
|
+
if (kept.length === 0) return [{
|
|
6833
|
+
name: IMPLICIT_SLICE,
|
|
6834
|
+
base: episode.base,
|
|
6835
|
+
tip: episode.tip,
|
|
6836
|
+
open: true
|
|
6837
|
+
}];
|
|
6838
|
+
return kept.map((s, i) => ({
|
|
6839
|
+
name: s.name,
|
|
6840
|
+
base: i === 0 ? episode.base : kept[i - 1].leftAt ?? episode.base,
|
|
6841
|
+
tip: i === kept.length - 1 ? episode.tip : s.leftAt ?? episode.tip,
|
|
6842
|
+
open: i === kept.length - 1
|
|
6843
|
+
}));
|
|
6844
|
+
};
|
|
6845
|
+
//#endregion
|
|
6820
6846
|
//#region src/replay/Tree.ts
|
|
6821
6847
|
const treeFromRecord = (files) => ({
|
|
6822
6848
|
paths: () => Object.keys(files).sort(),
|
|
@@ -6973,7 +6999,7 @@ const TRAILER_READERS = {
|
|
|
6973
6999
|
if (value !== "") into.branch ??= value;
|
|
6974
7000
|
},
|
|
6975
7001
|
"Gtd-Version": (value, into) => {
|
|
6976
|
-
if (value !== "") into.version ??= value;
|
|
7002
|
+
if (value !== "" && !/[\u0000-\u001f\u007f-\u009f]/.test(value)) into.version ??= value;
|
|
6977
7003
|
},
|
|
6978
7004
|
"Gtd-Kept": (value, into) => {
|
|
6979
7005
|
if (HASH_RE.test(value)) into.kept ??= value;
|
|
@@ -7164,6 +7190,7 @@ const replay = async (input) => {
|
|
|
7164
7190
|
const keptTips = /* @__PURE__ */ new Map();
|
|
7165
7191
|
let landed;
|
|
7166
7192
|
const marks = [];
|
|
7193
|
+
const slices = [];
|
|
7167
7194
|
let outcome;
|
|
7168
7195
|
let signal;
|
|
7169
7196
|
const settled = new Promise((resolve) => {
|
|
@@ -7370,7 +7397,8 @@ const replay = async (input) => {
|
|
|
7370
7397
|
rest: step,
|
|
7371
7398
|
trace,
|
|
7372
7399
|
landed,
|
|
7373
|
-
marks
|
|
7400
|
+
marks,
|
|
7401
|
+
slices: slices.map((s) => ({ ...s }))
|
|
7374
7402
|
});
|
|
7375
7403
|
return landPending(step, input.pending);
|
|
7376
7404
|
};
|
|
@@ -7409,7 +7437,8 @@ const replay = async (input) => {
|
|
|
7409
7437
|
via,
|
|
7410
7438
|
trace,
|
|
7411
7439
|
landed,
|
|
7412
|
-
marks
|
|
7440
|
+
marks,
|
|
7441
|
+
slices: slices.map((s) => ({ ...s }))
|
|
7413
7442
|
};
|
|
7414
7443
|
const changesSince = (hash) => {
|
|
7415
7444
|
const tree = treeAt(hash);
|
|
@@ -7434,6 +7463,31 @@ const replay = async (input) => {
|
|
|
7434
7463
|
scopes.pop();
|
|
7435
7464
|
scopeCallIds.pop();
|
|
7436
7465
|
},
|
|
7466
|
+
enterSlice: (name) => {
|
|
7467
|
+
if (outcome !== void 0) return;
|
|
7468
|
+
const fail = (message) => {
|
|
7469
|
+
finish({
|
|
7470
|
+
kind: "failed",
|
|
7471
|
+
message
|
|
7472
|
+
});
|
|
7473
|
+
throw new Stop();
|
|
7474
|
+
};
|
|
7475
|
+
if (typeof name !== "string" || !SLICE_NAME.test(name)) fail(`gtd: slice "${name}": name must match ${SLICE_NAME}`);
|
|
7476
|
+
if (name === "main") fail(`gtd: slice "${IMPLICIT_SLICE}" is reserved for the implicit slice`);
|
|
7477
|
+
const open = slices.find((s) => s.leftAt === void 0);
|
|
7478
|
+
if (open !== void 0) fail(`gtd: slice "${name}" is entered inside slice "${open.name}" — slices do not nest`);
|
|
7479
|
+
if (slices.some((s) => s.name === name)) fail(`gtd: slice "${name}" is entered twice in this episode — a loop must sit inside its slice`);
|
|
7480
|
+
slices.push({
|
|
7481
|
+
name,
|
|
7482
|
+
enteredAt: position.hash,
|
|
7483
|
+
leftAt: void 0
|
|
7484
|
+
});
|
|
7485
|
+
},
|
|
7486
|
+
leaveSlice: () => {
|
|
7487
|
+
if (outcome !== void 0) return;
|
|
7488
|
+
const open = slices.find((s) => s.leftAt === void 0);
|
|
7489
|
+
if (open !== void 0) open.leftAt = position.hash;
|
|
7490
|
+
},
|
|
7437
7491
|
read: (path) => position.tree.read(path),
|
|
7438
7492
|
glob: (pattern) => position.tree.paths().filter((path) => globMatches(path, pattern)),
|
|
7439
7493
|
changes: () => changesBetween(previousPosition.tree, position.tree),
|
|
@@ -8814,22 +8868,14 @@ const runAtTip = (git, def, tip) => gen(function* () {
|
|
|
8814
8868
|
if (closing.length === 0) closing = yield* git.commitHistory(void 0, tip.hash);
|
|
8815
8869
|
return yield* runOf([...before, ...closing], { ...location }, tip.hash);
|
|
8816
8870
|
});
|
|
8871
|
+
const runAtHead = (git, def) => gen(function* () {
|
|
8872
|
+
return yield* runAtTip(git, def, (yield* git.subjectHistory(1, 0))[0]);
|
|
8873
|
+
});
|
|
8817
8874
|
/** The process HEAD closes or sits inside — `gtd summary`'s run. */
|
|
8818
8875
|
const summaryRun = gen(function* () {
|
|
8819
8876
|
const git = yield* GitService;
|
|
8820
8877
|
const def = (yield* (yield* ConfigService).load).workflow;
|
|
8821
|
-
return yield*
|
|
8822
|
-
});
|
|
8823
|
-
/** The process whose finishing commit is `tip` — reads from `tip`'s own ancestry, so it works when no branch reaches it. */
|
|
8824
|
-
const closedRunAt = (tip) => gen(function* () {
|
|
8825
|
-
const git = yield* GitService;
|
|
8826
|
-
const def = (yield* (yield* ConfigService).load).workflow;
|
|
8827
|
-
const hash = yield* git.resolveRef(tip);
|
|
8828
|
-
const subject = yield* git.lastCommitSubject(hash);
|
|
8829
|
-
return yield* runAtTip(git, def, {
|
|
8830
|
-
hash,
|
|
8831
|
-
subject
|
|
8832
|
-
});
|
|
8878
|
+
return yield* runAtHead(git, def);
|
|
8833
8879
|
});
|
|
8834
8880
|
const commitTree = (workspace, hash) => {
|
|
8835
8881
|
let entries;
|
|
@@ -9112,6 +9158,10 @@ const pendingChanges = (git, withIndex) => gen(function* () {
|
|
|
9112
9158
|
const seen = new Set(worktree.map((e) => e.path));
|
|
9113
9159
|
return [...worktree, ...(yield* git.stagedPaths()).filter((e) => !seen.has(e.path))];
|
|
9114
9160
|
});
|
|
9161
|
+
const sliceRangesOf = (slices, run) => sliceRanges(slices, {
|
|
9162
|
+
base: run.startParentHash,
|
|
9163
|
+
tip: run.trace.at(-1)?.hash ?? run.startParentHash
|
|
9164
|
+
});
|
|
9115
9165
|
/** Resolve the rest at `ref`, or at HEAD when `ref` is `undefined`. */
|
|
9116
9166
|
const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
9117
9167
|
const git = yield* GitService;
|
|
@@ -9159,6 +9209,49 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
|
9159
9209
|
});
|
|
9160
9210
|
const currentRest = restAt(void 0);
|
|
9161
9211
|
/**
|
|
9212
|
+
* The slices of the process HEAD closes or sits inside, current episode only,
|
|
9213
|
+
* stack order. Reads like `historyTrail`, never `restAt`: it narrates and
|
|
9214
|
+
* writes nothing, and returns a version warning for a closed run.
|
|
9215
|
+
*/
|
|
9216
|
+
const processSlices = gen(function* () {
|
|
9217
|
+
const git = yield* GitService;
|
|
9218
|
+
const config = yield* (yield* ConfigService).load;
|
|
9219
|
+
const workspace = yield* Workspace;
|
|
9220
|
+
const host = yield* Host;
|
|
9221
|
+
const run = yield* runAtHead(git, config.workflow);
|
|
9222
|
+
if (run.trace.length === 0) return yield* fail$5(/* @__PURE__ */ new Error("gtd slices: refused — HEAD neither closes nor sits inside a process"));
|
|
9223
|
+
const closing = run.closingHash;
|
|
9224
|
+
let outcome;
|
|
9225
|
+
let versionWarning;
|
|
9226
|
+
if (closing !== void 0) {
|
|
9227
|
+
const replayed = yield* replayFinishedProcess(config, run, closing, {
|
|
9228
|
+
command: "gtd slices",
|
|
9229
|
+
configReason: "HEAD's process replays under today's config; revert or stash the edit to list its slices",
|
|
9230
|
+
notEnded: (o) => slicesDivergence(run, o)
|
|
9231
|
+
});
|
|
9232
|
+
outcome = replayed.outcome;
|
|
9233
|
+
versionWarning = replayed.versionWarning;
|
|
9234
|
+
} else {
|
|
9235
|
+
const { setup } = yield* setupFor(config, run, workspace, host);
|
|
9236
|
+
outcome = yield* promise(() => replayFor(setup));
|
|
9237
|
+
if (outcome.kind !== "rest") return yield* fail$5(slicesDivergence(run, outcome));
|
|
9238
|
+
}
|
|
9239
|
+
return {
|
|
9240
|
+
slices: sliceRangesOf(outcome.slices, run).map(({ name, base, tip, open }) => ({
|
|
9241
|
+
name,
|
|
9242
|
+
base,
|
|
9243
|
+
tip,
|
|
9244
|
+
closed: closing !== void 0 || !open
|
|
9245
|
+
})),
|
|
9246
|
+
...versionWarning !== void 0 ? { versionWarning } : {}
|
|
9247
|
+
};
|
|
9248
|
+
});
|
|
9249
|
+
/** Every `gtd slices` refusal reads `gtd slices: refused — <reason>`; replay's own `gtd: ` lead is dropped. */
|
|
9250
|
+
const slicesDivergence = (run, outcome) => {
|
|
9251
|
+
const reason = outcome.kind === "rest" && run.closingHash !== void 0 ? `${run.closingHash.slice(0, 7)} closes a process that does not end under today's config` : replayError(outcome)?.message.replace(/^gtd: /, "") ?? "replay failed";
|
|
9252
|
+
return /* @__PURE__ */ new Error(`gtd slices: refused — ${reason}`);
|
|
9253
|
+
};
|
|
9254
|
+
/**
|
|
9162
9255
|
* Whether `gtd --workflow <name>` can start a process here: the workflow must
|
|
9163
9256
|
* reach a step from the tree the opening commit would capture. That step is
|
|
9164
9257
|
* what the opening commit's subject names.
|
|
@@ -9392,7 +9485,7 @@ const inRepoConfigPaths = (root, files, recorded) => {
|
|
|
9392
9485
|
for (const file of files) paths.add(relative(root, resolve(root, file)).split(sep).join("/"));
|
|
9393
9486
|
return [...paths].filter((p) => p !== "" && !p.startsWith("..") && !isAbsolute(p)).sort();
|
|
9394
9487
|
};
|
|
9395
|
-
const configRefusal = (tip, paths) => /* @__PURE__ */ new Error(
|
|
9488
|
+
const configRefusal = (command, tip, paths, reason = "A history ref replays under today's config, so it is unreadable after the config changes") => /* @__PURE__ */ new Error(`${command}: refused — config differs at ${tip.slice(0, 7)}: ${paths.join(", ")}. ${reason}`);
|
|
9396
9489
|
const differingConfigPaths = (git, workspace, root, config, run, hash) => gen(function* () {
|
|
9397
9490
|
const differing = [];
|
|
9398
9491
|
for (const path of inRepoConfigPaths(root, Object.keys(config.fingerprint), run.pinnedConfig?.files ?? {})) if ((yield* orElseSucceed(git.readFileAtRef(hash, path), () => void 0)) !== workspace.readSync(path)) differing.push(path);
|
|
@@ -9423,22 +9516,19 @@ const versionWarningFor = (recorded) => recorded === GTD_VERSION ? void 0 : `gtd
|
|
|
9423
9516
|
const historyTrail = (tip) => gen(function* () {
|
|
9424
9517
|
const git = yield* GitService;
|
|
9425
9518
|
const config = yield* (yield* ConfigService).load;
|
|
9426
|
-
const workspace = yield* Workspace;
|
|
9427
|
-
const host = yield* Host;
|
|
9428
9519
|
const hash = yield* git.resolveRef(tip);
|
|
9429
|
-
const
|
|
9430
|
-
const
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
const outcome = yield*
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
const versionWarning = versionWarningFor(recorded);
|
|
9520
|
+
const subject = yield* git.lastCommitSubject(hash);
|
|
9521
|
+
const run = yield* runAtTip(git, config.workflow, {
|
|
9522
|
+
hash,
|
|
9523
|
+
subject
|
|
9524
|
+
});
|
|
9525
|
+
const { outcome, recorded, versionWarning } = yield* replayFinishedProcess(config, run, hash, {
|
|
9526
|
+
command: "gtd history",
|
|
9527
|
+
notEnded: (o) => notClosedError(hash, o)
|
|
9528
|
+
});
|
|
9439
9529
|
return {
|
|
9440
9530
|
tip: hash,
|
|
9441
|
-
steps,
|
|
9531
|
+
steps: historySteps(run),
|
|
9442
9532
|
ended: outcome.via,
|
|
9443
9533
|
version: {
|
|
9444
9534
|
recorded,
|
|
@@ -9447,6 +9537,24 @@ const historyTrail = (tip) => gen(function* () {
|
|
|
9447
9537
|
...versionWarning !== void 0 ? { versionWarning } : {}
|
|
9448
9538
|
};
|
|
9449
9539
|
});
|
|
9540
|
+
/** Refuses on config drift before replaying, so a stale config never runs the flow. */
|
|
9541
|
+
const replayFinishedProcess = (config, run, hash, how) => gen(function* () {
|
|
9542
|
+
const git = yield* GitService;
|
|
9543
|
+
const workspace = yield* Workspace;
|
|
9544
|
+
const host = yield* Host;
|
|
9545
|
+
const differing = yield* differingConfigPaths(git, workspace, host.root, config, run, hash);
|
|
9546
|
+
if (differing.length > 0) return yield* fail$5(configRefusal(how.command, hash, differing, how.configReason));
|
|
9547
|
+
const { setup } = yield* setupFor(config, run, workspace, host);
|
|
9548
|
+
const outcome = yield* promise(() => replayFor(setup));
|
|
9549
|
+
if (outcome.kind !== "ended") return yield* fail$5(how.notEnded(outcome));
|
|
9550
|
+
const recorded = parseCommitMessage(run.episode.commits.find((c) => c.hash === hash)?.message ?? "").version;
|
|
9551
|
+
const versionWarning = versionWarningFor(recorded);
|
|
9552
|
+
return {
|
|
9553
|
+
outcome,
|
|
9554
|
+
recorded,
|
|
9555
|
+
...versionWarning !== void 0 ? { versionWarning } : {}
|
|
9556
|
+
};
|
|
9557
|
+
});
|
|
9450
9558
|
//#endregion
|
|
9451
9559
|
//#region src/emit/GitScript.ts
|
|
9452
9560
|
const shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
@@ -10315,6 +10423,17 @@ const runDoorsCommand = (json, out) => gen(function* () {
|
|
|
10315
10423
|
if (json.kind === "document") out.write(`${JSON.stringify(document)}\n`);
|
|
10316
10424
|
else yield* writeSelection(out, document, json.path);
|
|
10317
10425
|
});
|
|
10426
|
+
const runSlicesCommand = (json, out) => gen(function* () {
|
|
10427
|
+
const { slices, versionWarning } = yield* processSlices;
|
|
10428
|
+
if (versionWarning !== void 0) yield* (yield* Narrator).warn(versionWarning);
|
|
10429
|
+
if (json.kind === "off") {
|
|
10430
|
+
out.write(slices.map((s) => `${s.name} ${s.base}..${s.tip}\n`).join(""));
|
|
10431
|
+
return;
|
|
10432
|
+
}
|
|
10433
|
+
const document = slicesDocument(slices);
|
|
10434
|
+
if (json.kind === "document") out.write(renderSlicesJson(document));
|
|
10435
|
+
else yield* writeSelection(out, document, json.path);
|
|
10436
|
+
});
|
|
10318
10437
|
/**
|
|
10319
10438
|
* Ends the process underway without completing it. Nothing is discarded: a
|
|
10320
10439
|
* `git reset --mixed` to the process's start commit leaves every turn
|
|
@@ -10650,6 +10769,7 @@ const dispatchVoidCommand = (command, json, out) => {
|
|
|
10650
10769
|
case "start": return runStartCommand(command.actor, command.workflow, command.vars, out, command.label);
|
|
10651
10770
|
case "door": return runDoorCommand(command.name, command.args, out);
|
|
10652
10771
|
case "doors": return runDoorsCommand(json, out);
|
|
10772
|
+
case "slices": return runSlicesCommand(json, out);
|
|
10653
10773
|
case "abandon": return runAbandonCommand(out);
|
|
10654
10774
|
case "restore": return runRestoreCommand(out);
|
|
10655
10775
|
case "next": return runNextCommand(json, out);
|
|
@@ -10706,12 +10826,12 @@ const FLAGS = [
|
|
|
10706
10826
|
name: "--json",
|
|
10707
10827
|
arity: "optional",
|
|
10708
10828
|
repeatable: false,
|
|
10709
|
-
scope: (kind) => kind === "next" || kind === "land" || kind === "judge" || kind === "judgeAnswer" || kind === "doors" || kind === "historyList" || kind === "historyShow" || kind === "strays",
|
|
10829
|
+
scope: (kind) => kind === "next" || kind === "land" || kind === "judge" || kind === "judgeAnswer" || kind === "doors" || kind === "slices" || kind === "historyList" || kind === "historyShow" || kind === "strays",
|
|
10710
10830
|
decode: ([raw]) => right(raw ?? ""),
|
|
10711
|
-
scopeError: "gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer`/`gtd doors`/`gtd history list`/`gtd history show`/`gtd strays` — every other command prints plain text; see `gtd install` for the driver protocol briefing",
|
|
10831
|
+
scopeError: "gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer`/`gtd doors`/`gtd slices`/`gtd history list`/`gtd history show`/`gtd strays` — every other command prints plain text; see `gtd install` for the driver protocol briefing",
|
|
10712
10832
|
valueHint: "<path>",
|
|
10713
10833
|
help: [
|
|
10714
|
-
"(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd history",
|
|
10834
|
+
"(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd slices/gtd history",
|
|
10715
10835
|
"list/gtd history show/gtd strays only) output structured JSON. Bare --json prints the whole document;",
|
|
10716
10836
|
"--json=<path> (a dotted key path into that document, e.g.",
|
|
10717
10837
|
"kind, content, session.id) prints just that value: a",
|
|
@@ -11181,6 +11301,25 @@ const COMMAND_ROWS = [
|
|
|
11181
11301
|
"value off it. Writes nothing"
|
|
11182
11302
|
]
|
|
11183
11303
|
},
|
|
11304
|
+
{
|
|
11305
|
+
token: "slices",
|
|
11306
|
+
kind: "slices",
|
|
11307
|
+
arity: "none",
|
|
11308
|
+
details: [
|
|
11309
|
+
"List the slices of the process HEAD closes or sits inside,",
|
|
11310
|
+
"first slice first: one `name base..tip` line each (full",
|
|
11311
|
+
"hashes; plain output carries no promise). --json prints",
|
|
11312
|
+
"{ schema: 1, slices: [{ name, base, tip, closed }] };",
|
|
11313
|
+
"--json=<path> reads one value off it. Writes nothing: no",
|
|
11314
|
+
"git, no state transition, no session identity. When HEAD",
|
|
11315
|
+
"closes a process, every slice is listed as closed; a gtd",
|
|
11316
|
+
"version mismatch only warns on stderr. Refuses (exit 1)",
|
|
11317
|
+
"when HEAD neither closes nor sits inside a process, or when",
|
|
11318
|
+
"today's config disagrees with history about the process:",
|
|
11319
|
+
"HEAD closes it and its config files differ from the working",
|
|
11320
|
+
"tree, or the replay diverged"
|
|
11321
|
+
]
|
|
11322
|
+
},
|
|
11184
11323
|
{
|
|
11185
11324
|
token: "judge",
|
|
11186
11325
|
kind: "judge",
|
package/dist/wire.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
export { ACCESS_REFUSAL, BEAT_KINDS, DEMAND_BRIEFING, EXIT_CODES, EXIT_OK, EXIT_RUNTIME_ERROR, EXIT_SIGINT, EXIT_SIGTERM, EXIT_STRAYS_FOUND, EXIT_USAGE_ERROR, UNATTRIBUTED_MODEL, WIRE_SCHEMA, beatDocument, beatKindOf, demandOf, globMatches, judgeJson, landFields, landProseText, noopText, renderBeatJson, renderBeatPlain, renderFormat, renderLandJson, renderLandPlain, resolveSession, selectPath, stallDiagnosis, statusOf };
|
|
1
|
+
import { A as WIRE_SCHEMA, C as EXIT_RUNTIME_ERROR, D as EXIT_USAGE_ERROR, E as EXIT_STRAYS_FOUND, O as ACCESS_REFUSAL, S as EXIT_OK, T as EXIT_SIGTERM, _ as BEAT_KINDS, a as selectPath, b as stallDiagnosis, c as landProseText, d as renderLandPlain, f as DEMAND_BRIEFING, g as statusOf, h as renderBeatJson, i as resolveSession, j as renderFormat, k as UNATTRIBUTED_MODEL, l as noopText, m as beatDocument, n as slicesDocument, o as judgeJson, p as renderBeatPlain, r as globMatches, s as landFields, t as renderSlicesJson, u as renderLandJson, v as beatKindOf, w as EXIT_SIGINT, x as EXIT_CODES, y as demandOf } from "./chunks/wire-D3Rc5r22.mjs";
|
|
2
|
+
export { ACCESS_REFUSAL, BEAT_KINDS, DEMAND_BRIEFING, EXIT_CODES, EXIT_OK, EXIT_RUNTIME_ERROR, EXIT_SIGINT, EXIT_SIGTERM, EXIT_STRAYS_FOUND, EXIT_USAGE_ERROR, UNATTRIBUTED_MODEL, WIRE_SCHEMA, beatDocument, beatKindOf, demandOf, globMatches, judgeJson, landFields, landProseText, noopText, renderBeatJson, renderBeatPlain, renderFormat, renderLandJson, renderLandPlain, renderSlicesJson, resolveSession, selectPath, slicesDocument, stallDiagnosis, statusOf };
|
package/package.json
CHANGED
package/src/cli/Cli.test.ts
CHANGED
|
@@ -112,6 +112,7 @@ describe("parseArgv — scope", () => {
|
|
|
112
112
|
["judge", "--json"],
|
|
113
113
|
["judge", "answer", "--json"],
|
|
114
114
|
["doors", "--json"],
|
|
115
|
+
["slices", "--json"],
|
|
115
116
|
]) {
|
|
116
117
|
const ok = parseArgv(["node", "gtd.js", ...args])
|
|
117
118
|
expect(ok.kind).toBe("command")
|
|
@@ -927,6 +928,40 @@ describe("parseArgv — doors", () => {
|
|
|
927
928
|
})
|
|
928
929
|
})
|
|
929
930
|
|
|
931
|
+
describe("parseArgv — slices", () => {
|
|
932
|
+
const parse = (...args: string[]): CliPlan => parseArgv(["node", "gtd.js", ...args])
|
|
933
|
+
|
|
934
|
+
it("`gtd slices` parses with every --json shape", () => {
|
|
935
|
+
expect(parse("slices")).toMatchObject({
|
|
936
|
+
kind: "command",
|
|
937
|
+
command: { kind: "slices" },
|
|
938
|
+
json: { kind: "off" },
|
|
939
|
+
})
|
|
940
|
+
expect(parse("slices", "--json")).toMatchObject({ json: { kind: "document" } })
|
|
941
|
+
expect(parse("slices", "--json=slices.0.name")).toMatchObject({
|
|
942
|
+
json: { kind: "select", path: "slices.0.name" },
|
|
943
|
+
})
|
|
944
|
+
})
|
|
945
|
+
|
|
946
|
+
it("`gtd slices` takes no argument", () => {
|
|
947
|
+
const plan = parse("slices", "extra")
|
|
948
|
+
expect(plan.kind).toBe("usage")
|
|
949
|
+
if (plan.kind === "usage") expect(plan.message).toContain("too many arguments")
|
|
950
|
+
})
|
|
951
|
+
|
|
952
|
+
it("help lists the command, and --json names it", () => {
|
|
953
|
+
const help = renderHelp()
|
|
954
|
+
expect(help).toMatch(/^ {2}slices\b/m)
|
|
955
|
+
expect(help).toContain("gtd slices")
|
|
956
|
+
})
|
|
957
|
+
|
|
958
|
+
it("--json's scope error names gtd slices", () => {
|
|
959
|
+
const plan = parse("lsp", "--json")
|
|
960
|
+
expect(plan.kind).toBe("usage")
|
|
961
|
+
if (plan.kind === "usage") expect(plan.message).toContain("`gtd slices`")
|
|
962
|
+
})
|
|
963
|
+
})
|
|
964
|
+
|
|
930
965
|
describe("parseArgv — gtd history", () => {
|
|
931
966
|
const parse = (...args: string[]) => parseArgv(["node", "gtd.js", ...args])
|
|
932
967
|
const usage = (plan: CliPlan): string => (plan.kind === "usage" ? plan.message : "")
|
|
@@ -1057,6 +1092,7 @@ describe("standaloneKinds / needsOf", () => {
|
|
|
1057
1092
|
"judgeAnswer",
|
|
1058
1093
|
"door",
|
|
1059
1094
|
"doors",
|
|
1095
|
+
"slices",
|
|
1060
1096
|
"historyList",
|
|
1061
1097
|
"historyShow",
|
|
1062
1098
|
"historyFetch",
|
|
@@ -1136,9 +1172,9 @@ describe("renderHelp", () => {
|
|
|
1136
1172
|
expect(match, file).not.toBeNull()
|
|
1137
1173
|
return new Set([...match![1]!.matchAll(/`([a-z]+(?: [a-z]+)?)`/g)].map(([, name]) => name!))
|
|
1138
1174
|
}
|
|
1139
|
-
expect(
|
|
1140
|
-
commands,
|
|
1141
|
-
)
|
|
1175
|
+
expect(
|
|
1176
|
+
listed("../../README.md", /The `gtd` bin's commands — ([\s\S]*?) — are\s+listed/),
|
|
1177
|
+
).toEqual(commands)
|
|
1142
1178
|
expect(listed("../../COMPATIBILITY.md", /every command \(([\s\S]*?)\), option/)).toEqual(
|
|
1143
1179
|
commands,
|
|
1144
1180
|
)
|
package/src/cli/Cli.ts
CHANGED
|
@@ -101,16 +101,17 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
101
101
|
kind === "judge" ||
|
|
102
102
|
kind === "judgeAnswer" ||
|
|
103
103
|
kind === "doors" ||
|
|
104
|
+
kind === "slices" ||
|
|
104
105
|
kind === "historyList" ||
|
|
105
106
|
kind === "historyShow" ||
|
|
106
107
|
kind === "strays",
|
|
107
108
|
decode: ([raw]) => Either.right(raw ?? ""),
|
|
108
109
|
scopeError:
|
|
109
|
-
"gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer`/`gtd doors`/`gtd history list`/`gtd history show`/`gtd strays` — " +
|
|
110
|
+
"gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer`/`gtd doors`/`gtd slices`/`gtd history list`/`gtd history show`/`gtd strays` — " +
|
|
110
111
|
"every other command prints plain text; see `gtd install` for the driver protocol briefing",
|
|
111
112
|
valueHint: "<path>",
|
|
112
113
|
help: [
|
|
113
|
-
"(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd history",
|
|
114
|
+
"(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd slices/gtd history",
|
|
114
115
|
"list/gtd history show/gtd strays only) output structured JSON. Bare --json prints the whole document;",
|
|
115
116
|
"--json=<path> (a dotted key path into that document, e.g.",
|
|
116
117
|
"kind, content, session.id) prints just that value: a",
|
|
@@ -636,6 +637,25 @@ const COMMAND_ROWS: readonly CommandRow[] = [
|
|
|
636
637
|
"value off it. Writes nothing",
|
|
637
638
|
],
|
|
638
639
|
},
|
|
640
|
+
{
|
|
641
|
+
token: "slices",
|
|
642
|
+
kind: "slices",
|
|
643
|
+
arity: "none",
|
|
644
|
+
details: [
|
|
645
|
+
"List the slices of the process HEAD closes or sits inside,",
|
|
646
|
+
"first slice first: one `name base..tip` line each (full",
|
|
647
|
+
"hashes; plain output carries no promise). --json prints",
|
|
648
|
+
"{ schema: 1, slices: [{ name, base, tip, closed }] };",
|
|
649
|
+
"--json=<path> reads one value off it. Writes nothing: no",
|
|
650
|
+
"git, no state transition, no session identity. When HEAD",
|
|
651
|
+
"closes a process, every slice is listed as closed; a gtd",
|
|
652
|
+
"version mismatch only warns on stderr. Refuses (exit 1)",
|
|
653
|
+
"when HEAD neither closes nor sits inside a process, or when",
|
|
654
|
+
"today's config disagrees with history about the process:",
|
|
655
|
+
"HEAD closes it and its config files differ from the working",
|
|
656
|
+
"tree, or the replay diverged",
|
|
657
|
+
],
|
|
658
|
+
},
|
|
639
659
|
{
|
|
640
660
|
token: "judge",
|
|
641
661
|
kind: "judge",
|
|
@@ -1304,6 +1324,7 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1304
1324
|
| "install"
|
|
1305
1325
|
| "loop"
|
|
1306
1326
|
| "doors"
|
|
1327
|
+
| "slices"
|
|
1307
1328
|
| "summary"
|
|
1308
1329
|
| "base"
|
|
1309
1330
|
| "judge"
|
package/src/cli/Program.test.ts
CHANGED
|
@@ -1677,6 +1677,7 @@ describe("runCommand — refuses in a repository with no commits", () => {
|
|
|
1677
1677
|
start: { kind: "start", actor: "human", workflow: "feature", vars: {}, label: "" },
|
|
1678
1678
|
door: { kind: "door", name: "fix", args: [] },
|
|
1679
1679
|
doors: { kind: "doors" },
|
|
1680
|
+
slices: { kind: "slices" },
|
|
1680
1681
|
abandon: { kind: "abandon" },
|
|
1681
1682
|
restore: { kind: "restore" },
|
|
1682
1683
|
next: { kind: "next" },
|
|
@@ -1703,13 +1704,14 @@ describe("runCommand — refuses in a repository with no commits", () => {
|
|
|
1703
1704
|
const NO_COMMITS_MESSAGE =
|
|
1704
1705
|
"gtd requires a repository with at least one commit — make an initial commit, then run gtd again"
|
|
1705
1706
|
|
|
1706
|
-
it("derives exactly the
|
|
1707
|
+
it("derives exactly the seventeen non-standalone kinds — a canary for the table-driven cases below", () => {
|
|
1707
1708
|
expect(stateKinds.sort()).toEqual(
|
|
1708
1709
|
[
|
|
1709
1710
|
"abandon",
|
|
1710
1711
|
"base",
|
|
1711
1712
|
"door",
|
|
1712
1713
|
"doors",
|
|
1714
|
+
"slices",
|
|
1713
1715
|
"start",
|
|
1714
1716
|
"land",
|
|
1715
1717
|
"next",
|
package/src/cli/Program.ts
CHANGED
|
@@ -43,6 +43,8 @@ import {
|
|
|
43
43
|
type StatusChange,
|
|
44
44
|
selectPath,
|
|
45
45
|
EXIT_OK,
|
|
46
|
+
renderSlicesJson,
|
|
47
|
+
slicesDocument,
|
|
46
48
|
} from "../wire/index.js"
|
|
47
49
|
import {
|
|
48
50
|
currentRest,
|
|
@@ -51,6 +53,7 @@ import {
|
|
|
51
53
|
renderRest,
|
|
52
54
|
restAt,
|
|
53
55
|
previewLanding,
|
|
56
|
+
processSlices,
|
|
54
57
|
noProcessUnderway,
|
|
55
58
|
restIsIdle,
|
|
56
59
|
reviewBaseFor,
|
|
@@ -143,6 +146,7 @@ export type Command =
|
|
|
143
146
|
}
|
|
144
147
|
| { readonly kind: "door"; readonly name: string; readonly args: readonly string[] }
|
|
145
148
|
| { readonly kind: "doors" }
|
|
149
|
+
| { readonly kind: "slices" }
|
|
146
150
|
| { readonly kind: "abandon" }
|
|
147
151
|
| { readonly kind: "restore" }
|
|
148
152
|
| { readonly kind: "next" }
|
|
@@ -659,6 +663,22 @@ const runDoorsCommand = (
|
|
|
659
663
|
else yield* writeSelection(out, document, json.path)
|
|
660
664
|
})
|
|
661
665
|
|
|
666
|
+
const runSlicesCommand = (
|
|
667
|
+
json: JsonMode,
|
|
668
|
+
out: ArtifactOut,
|
|
669
|
+
): Effect.Effect<void, Error, CommandRequirements> =>
|
|
670
|
+
Effect.gen(function* () {
|
|
671
|
+
const { slices, versionWarning } = yield* processSlices
|
|
672
|
+
if (versionWarning !== undefined) yield* (yield* Narrator).warn(versionWarning)
|
|
673
|
+
if (json.kind === "off") {
|
|
674
|
+
out.write(slices.map((s) => `${s.name} ${s.base}..${s.tip}\n`).join(""))
|
|
675
|
+
return
|
|
676
|
+
}
|
|
677
|
+
const document = slicesDocument(slices)
|
|
678
|
+
if (json.kind === "document") out.write(renderSlicesJson(document))
|
|
679
|
+
else yield* writeSelection(out, document, json.path)
|
|
680
|
+
})
|
|
681
|
+
|
|
662
682
|
/**
|
|
663
683
|
* Ends the process underway without completing it. Nothing is discarded: a
|
|
664
684
|
* `git reset --mixed` to the process's start commit leaves every turn
|
|
@@ -1223,6 +1243,8 @@ const dispatchVoidCommand = (
|
|
|
1223
1243
|
return runDoorCommand(command.name, command.args, out)
|
|
1224
1244
|
case "doors":
|
|
1225
1245
|
return runDoorsCommand(json, out)
|
|
1246
|
+
case "slices":
|
|
1247
|
+
return runSlicesCommand(json, out)
|
|
1226
1248
|
case "abandon":
|
|
1227
1249
|
return runAbandonCommand(out)
|
|
1228
1250
|
case "restore":
|