@pmelab/gtd 26.1.0 → 27.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +16 -3
- package/dist/chunks/{Schema-B5yK7QrT.mjs → Schema-CyImoHTj.mjs} +56 -3
- package/dist/chunks/{reserved-6pAgFGgB.mjs → reserved-BL9krPao.mjs} +1 -1
- package/dist/chunks/{wire-DA7q2ikx.mjs → wire-BY1S-obW.mjs} +5 -2
- package/dist/gtd.bundle.mjs +85 -16
- package/dist/testkit.mjs +163 -17
- package/dist/wire.mjs +2 -2
- package/dist/workflows/feature.mjs +1 -1
- package/dist/workflows/fix.mjs +1 -1
- package/dist/workflows/review.mjs +1 -1
- package/package.json +2 -2
- package/src/cli/Cli.test.ts +53 -1
- package/src/cli/Cli.ts +48 -6
- package/src/cli/Output.ts +3 -1
- package/src/cli/Program.test.ts +1 -0
- package/src/cli/Program.ts +28 -7
- package/src/cli/Strays.test.ts +88 -0
- package/src/cli/Strays.ts +56 -0
- package/src/platform/Git.test.ts +96 -1
- package/src/platform/Git.ts +24 -0
- package/src/step/index.ts +1 -0
- package/src/step/strayPaths.test.ts +17 -0
- package/src/step/strayPaths.ts +8 -0
- package/src/testkit/FakeGitOperations.ts +2 -0
- package/src/testkit/GitTiers.ts +67 -1
- package/src/testkit/InMemRepo.ts +7 -0
- package/src/testkit/Layers.test.ts +34 -1
- package/src/testkit/Layers.ts +21 -1
- package/src/testkit/TestRepo.ts +1 -1
- package/src/wire/ExitCodes.test.ts +11 -3
- package/src/wire/ExitCodes.ts +3 -0
- package/src/wire/Glob.test.ts +35 -0
- package/src/wire/Glob.ts +1 -1
package/dist/testkit.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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 { E as ACCESS_REFUSAL, _ as demandOf, a as landFields, c as renderLandJson, d as renderBeatPlain, f as beatDocument, g as beatKindOf, i as judgeJson, k as renderFormat, l as renderLandPlain, m as statusOf, n as resolveSession, p as renderBeatJson, r as selectPath, s as noopText, t as globMatches } from "./chunks/wire-BY1S-obW.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
|
-
import { $ as
|
|
4
|
+
import { $ as logWarning, $i as format, $n as map$5, $r as make$19, $t as provide$1, A as catchIf, Ai as get$5, An as scopeMake, Ar as flatMap$3, At as suspend$5, B as flatMap$4, Bi as isNone, 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 splitAt, Fn as join$3, Fr as map$3, Ft as tryMap, G as forkIn, Gi as some, Gn as TypeId$10, Gr as whenEffect, Gt as zipLeft, H as fnUntraced, Hi as map$8, Hn as modify$1, Hr as uninterruptible, Ht as when, I as exit, Ii as filter$1, In as sequential, Ir as onInterrupt, It as tryPromise, J as ignore, Ji as match$2, Jn as all, Jr as DeferredTypeId$1, Jt as spaced$1, K as forkScoped, Ki as isLeft, Kn as Class, Kr as withFiberRuntime, Kt as zipRight$3, L as fail$5, Li as flatMap$5, Ln as bind, Lr as scopeClose, Lt as try_, M as die$2, Mi as omit, Mn as _await$2, Mr as interruptAsFiber, Mt as tap, N as dieMessage, Ni as empty$5, Nn as inheritAll$1, Nr as interruptFiber, Nt as tapErrorCause, O as catchAll$1, Oi as Tag, On as ensuring$3, Or as fiberId, Ot as serviceOption, P as either, Pi as fromIterable, Pn as interruptAllAs$1, Pr as interruptible$1, Pt as timeout, Q as isEffect, Qi as NodeInspectSymbol, Qn as isFailure, Qr as ids, Qt as mergeAll$2, R as failCause$4, Ri as fromNullable, 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 match$4, Un as set$1, Ur as uninterruptibleMask$1, Ut as withFiberRuntime$1, V as flatten$4, Vi as isSome, Vn as make$16, Vr as tap$1, Vt as unsafeMakeSemaphore$1, W as forkDaemon, Wi as none, Wn as update$1, Wr as void_$2, Wt as zip$1, X as interruptible, Xi as pipeArguments, Xn as fail$4, Xr as combine, Xt as tagged, Y as interrupt$4, Yi as right, Yn as die, Yr as empty$6, Yt as TaggedError$1, Z as intoDeferred, Zi as BaseProto, 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 constUndefined, 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 identity, 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 toJSON, 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 constTrue, ii as has, in as unsafeRunPromiseExit, ir as zip$2, it as match$5, j as catchTag, ji as getOption, jn as scopeTag, jr as interrupt$2, jt as sync$2, k as catchAllCause, ki as add, kn as forEachConcurrentDiscard, kr as fiberRefLocally, kt as succeed$6, l as Struct, la as pipe, 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 hasProperty, ni as make$17, nn as succeed$3, nr as succeed$5, nt as mapBoth$1, o as Never, oa as constVoid, 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 left, qn as spanTag, qr as zipRight$1, qt as zipWith, r as Cause, ra as constFalse, ri as set$2, rn as unsafeFork, rr as void_$3, rt as mapError, s as Number$, sa as dual, si as isFinite, sn as unsafeMakeSemaphore, sr as asVoid, st as matchEffect, t as Array$, ta as equals, ti as get$4, tn as scoped$3, tr as match$3, tt as map$4, u as TaggedError, 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 getOrElse, zn as sleep, zr as suspend$4, zt as uninterruptibleMask } from "./chunks/Schema-CyImoHTj.mjs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import * as Crypto from "node:crypto";
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
@@ -5401,6 +5401,7 @@ const makeGitImpl = (executor, root) => {
|
|
|
5401
5401
|
}))),
|
|
5402
5402
|
hasCommits: () => exec("git", "rev-parse", "--verify", "HEAD").pipe(map$4(() => true), catchAll$1(() => succeed$6(false))),
|
|
5403
5403
|
readFileAtRef: (ref, path) => exec("git", "show", `${ref}:${path}`),
|
|
5404
|
+
diffPaths: (base, tip) => exec("git", "diff", "--name-only", "-z", "--no-renames", "--no-relative", "--ignore-submodules=none", "--end-of-options", base, tip, "--").pipe(map$4(splitNul)),
|
|
5404
5405
|
changedPaths: (base) => gen(function* () {
|
|
5405
5406
|
const ref = base ?? "HEAD";
|
|
5406
5407
|
const nameStatusOut = yield* exec("git", "diff", "--name-status", "-z", ref).pipe(catchAll$1(() => succeed$6("")));
|
|
@@ -8467,6 +8468,10 @@ const resolveBase = (commandLabel, value) => gen(function* () {
|
|
|
8467
8468
|
return mergeBase.value;
|
|
8468
8469
|
});
|
|
8469
8470
|
//#endregion
|
|
8471
|
+
//#region src/step/strayPaths.ts
|
|
8472
|
+
/** Globs use the `access.write` dialect (`globMatches`). */
|
|
8473
|
+
const strayPaths = (touched, allow) => touched.filter((path) => !allow.some((glob) => globMatches(path, glob))).sort();
|
|
8474
|
+
//#endregion
|
|
8470
8475
|
//#region src/edge/Edge.ts
|
|
8471
8476
|
const EMPTY_TREE$1 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
8472
8477
|
const ACTORS = /* @__PURE__ */ new Set([
|
|
@@ -9763,6 +9768,24 @@ const writeSelection = (out, fields, path) => gen(function* () {
|
|
|
9763
9768
|
else if (selection.kind === "unknown") return yield* fail$5(new SelectorUsageError(`gtd: unknown --json selector "${selection.path}" — see \`gtd --help\``));
|
|
9764
9769
|
});
|
|
9765
9770
|
//#endregion
|
|
9771
|
+
//#region src/cli/Strays.ts
|
|
9772
|
+
const resolveSide = (git, side) => git.resolveRef(`${side}^{commit}`).pipe(mapError(() => new GtdError(`gtd strays: unknown revision '${side}'`)));
|
|
9773
|
+
/** stdout carries the array alone; the summary is ungated stderr so a quiet run still explains its exit code, the per-path lines are narration. */
|
|
9774
|
+
const runStraysCommand = (command, json, out) => gen(function* () {
|
|
9775
|
+
const git = yield* GitService;
|
|
9776
|
+
const narrator = yield* Narrator;
|
|
9777
|
+
const base = yield* resolveSide(git, command.base);
|
|
9778
|
+
const tip = yield* resolveSide(git, command.tip);
|
|
9779
|
+
const touched = yield* git.diffPaths(base, tip);
|
|
9780
|
+
const strays = strayPaths(touched, command.globs);
|
|
9781
|
+
const total = touched.length;
|
|
9782
|
+
if (json.kind === "select") yield* writeSelection(out, strays, json.path).pipe(catchIf((error) => error instanceof SelectorUsageError && /^\d+$/.test(json.path), () => _void));
|
|
9783
|
+
else out.write(`${JSON.stringify(strays)}\n`);
|
|
9784
|
+
for (const path of strays) yield* narrator.narrate(`stray: ${JSON.stringify(path)}`);
|
|
9785
|
+
yield* narrator.warn(strays.length === 0 ? `gtd strays: no strays — ${total} touched files, all inside the allowlist` : `gtd strays: ${strays.length} of ${total} touched files outside the allowlist`);
|
|
9786
|
+
return strays.length === 0 ? 0 : 10;
|
|
9787
|
+
});
|
|
9788
|
+
//#endregion
|
|
9766
9789
|
//#region src/cli/History.ts
|
|
9767
9790
|
/** Newest tip first. */
|
|
9768
9791
|
const historyEntries = gen(function* () {
|
|
@@ -10415,7 +10438,8 @@ const needsOf = (kind) => {
|
|
|
10415
10438
|
case "lsp":
|
|
10416
10439
|
case "install":
|
|
10417
10440
|
case "loop":
|
|
10418
|
-
case "judgeRun":
|
|
10441
|
+
case "judgeRun":
|
|
10442
|
+
case "strays": return "none";
|
|
10419
10443
|
case "check":
|
|
10420
10444
|
case "uncheck": return "fs";
|
|
10421
10445
|
default: return "state";
|
|
@@ -10424,10 +10448,13 @@ const needsOf = (kind) => {
|
|
|
10424
10448
|
/**
|
|
10425
10449
|
* Dispatches to the named `run*Command` handler for every `Command.kind` —
|
|
10426
10450
|
* the counterpart of `Cli.ts`'s `parseArgv`, which has already validated
|
|
10427
|
-
* every field a handler receives.
|
|
10428
|
-
*
|
|
10429
|
-
* `runCli` supplies `EXIT_OK` once, after this Effect succeeds.
|
|
10451
|
+
* every field a handler receives. Only `strays` chooses its exit code; every
|
|
10452
|
+
* other branch returns `Effect<void>` and resolves to `EXIT_OK`.
|
|
10430
10453
|
*/
|
|
10454
|
+
const dispatchCommand = (command, json, out) => {
|
|
10455
|
+
if (command.kind === "strays") return runStraysCommand(command, json, out);
|
|
10456
|
+
return as(dispatchVoidCommand(command, json, out), 0);
|
|
10457
|
+
};
|
|
10431
10458
|
const dispatchVoidCommand = (command, json, out) => {
|
|
10432
10459
|
switch (command.kind) {
|
|
10433
10460
|
case "lsp": return runLspCommand();
|
|
@@ -10467,7 +10494,7 @@ const dispatchVoidCommand = (command, json, out) => {
|
|
|
10467
10494
|
* so a refusal writes nothing and emits no script by construction.
|
|
10468
10495
|
*/
|
|
10469
10496
|
const runCommand$1 = (command, json, out) => {
|
|
10470
|
-
const dispatch =
|
|
10497
|
+
const dispatch = dispatchCommand(command, json, out);
|
|
10471
10498
|
if (needsOf(command.kind) !== "state") return dispatch;
|
|
10472
10499
|
return gen(function* () {
|
|
10473
10500
|
const git = yield* GitService;
|
|
@@ -10480,7 +10507,7 @@ const runCommand$1 = (command, json, out) => {
|
|
|
10480
10507
|
warn: () => _void
|
|
10481
10508
|
}));
|
|
10482
10509
|
for (const warning of config.warnings) yield* narrator.warn(`gtd: warning: ${formatDiagnostic(warning)}`);
|
|
10483
|
-
yield* dispatch;
|
|
10510
|
+
return yield* dispatch;
|
|
10484
10511
|
});
|
|
10485
10512
|
};
|
|
10486
10513
|
//#endregion
|
|
@@ -10495,13 +10522,13 @@ const FLAGS = [
|
|
|
10495
10522
|
name: "--json",
|
|
10496
10523
|
arity: "optional",
|
|
10497
10524
|
repeatable: false,
|
|
10498
|
-
scope: (kind) => kind === "next" || kind === "land" || kind === "judge" || kind === "judgeAnswer" || kind === "doors" || kind === "historyList" || kind === "historyShow",
|
|
10525
|
+
scope: (kind) => kind === "next" || kind === "land" || kind === "judge" || kind === "judgeAnswer" || kind === "doors" || kind === "historyList" || kind === "historyShow" || kind === "strays",
|
|
10499
10526
|
decode: ([raw]) => right(raw ?? ""),
|
|
10500
|
-
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",
|
|
10527
|
+
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",
|
|
10501
10528
|
valueHint: "<path>",
|
|
10502
10529
|
help: [
|
|
10503
10530
|
"(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd history",
|
|
10504
|
-
"list/gtd history show only) output structured JSON. Bare --json prints the whole document;",
|
|
10531
|
+
"list/gtd history show/gtd strays only) output structured JSON. Bare --json prints the whole document;",
|
|
10505
10532
|
"--json=<path> (a dotted key path into that document, e.g.",
|
|
10506
10533
|
"kind, content, session.id) prints just that value: a",
|
|
10507
10534
|
"scalar raw and unquoted, a boolean as true/false, a list",
|
|
@@ -11010,6 +11037,28 @@ const COMMAND_ROWS = [
|
|
|
11010
11037
|
"every question, with nothing on stdout"
|
|
11011
11038
|
]
|
|
11012
11039
|
},
|
|
11040
|
+
{
|
|
11041
|
+
token: "strays",
|
|
11042
|
+
kind: "strays",
|
|
11043
|
+
arity: {
|
|
11044
|
+
name: "range",
|
|
11045
|
+
variadic: "glob"
|
|
11046
|
+
},
|
|
11047
|
+
details: [
|
|
11048
|
+
"List the files <range> (<base>..<tip>, an empty side is HEAD)",
|
|
11049
|
+
"touched in its committed net diff that match none of the",
|
|
11050
|
+
"globs. Anchored at the repo root: * stays within one path",
|
|
11051
|
+
"segment, ** crosses them, everything else is literal — quote",
|
|
11052
|
+
"globs in the shell. stdout: a sorted JSON array, [] when",
|
|
11053
|
+
"clean; stderr: a summary, plus one stray: line per path with",
|
|
11054
|
+
"--verbose. --json=<n> picks one entry, nothing past the end;",
|
|
11055
|
+
"a symlink is a path like any other, never followed; a",
|
|
11056
|
+
"submodule counts as its path, whatever diff.ignoreSubmodules",
|
|
11057
|
+
"or .gitmodules ignore say. Runs in any git repository, no",
|
|
11058
|
+
"process needed. Exits 10 when",
|
|
11059
|
+
"strays were found, 1 on an unknown revision, 2 on a bad range"
|
|
11060
|
+
]
|
|
11061
|
+
},
|
|
11013
11062
|
{
|
|
11014
11063
|
token: "history list",
|
|
11015
11064
|
kind: "historyList",
|
|
@@ -11352,6 +11401,22 @@ const parseArgv = (argv) => {
|
|
|
11352
11401
|
json,
|
|
11353
11402
|
verbose
|
|
11354
11403
|
};
|
|
11404
|
+
if (kind === "strays") {
|
|
11405
|
+
const [range, ...globs] = restPositionals;
|
|
11406
|
+
const at = range.indexOf("..");
|
|
11407
|
+
if (at < 0 || range.includes("...") || range.indexOf("..", at + 1) >= 0) return usagePlan(`gtd strays: <range> must be <base>..<tip> (an empty side is HEAD) — got '${range}'`, jsonSeen);
|
|
11408
|
+
return {
|
|
11409
|
+
kind: "command",
|
|
11410
|
+
command: {
|
|
11411
|
+
kind: "strays",
|
|
11412
|
+
base: range.slice(0, at) || "HEAD",
|
|
11413
|
+
tip: range.slice(at + 2) || "HEAD",
|
|
11414
|
+
globs
|
|
11415
|
+
},
|
|
11416
|
+
json,
|
|
11417
|
+
verbose
|
|
11418
|
+
};
|
|
11419
|
+
}
|
|
11355
11420
|
if (kind === "uncheck") return {
|
|
11356
11421
|
kind: "command",
|
|
11357
11422
|
command: {
|
|
@@ -11430,9 +11495,9 @@ const runCli = (argv, io) => {
|
|
|
11430
11495
|
return flatMap$4(Host, ({ env }) => try_({
|
|
11431
11496
|
try: () => resolveFlags(env, io.flags),
|
|
11432
11497
|
catch: (e) => new GtdError(e.message)
|
|
11433
|
-
})).pipe(flatMap$4(() => runCommand$1(plan.command, plan.json, out)), map$4(() => {
|
|
11498
|
+
})).pipe(flatMap$4(() => runCommand$1(plan.command, plan.json, out)), map$4((code) => {
|
|
11434
11499
|
out.flush();
|
|
11435
|
-
io.exit(
|
|
11500
|
+
io.exit(code);
|
|
11436
11501
|
}), provide$2(io.layers(plan.verbose)), sandbox, catchAll$1((cause) => isInterruptedOnly(cause) ? failCause$4(cause) : report(io, plan.json.kind !== "off")(cause)));
|
|
11437
11502
|
};
|
|
11438
11503
|
//#endregion
|
|
@@ -11629,6 +11694,10 @@ var InMemRepo = class InMemRepo {
|
|
|
11629
11694
|
changedPathsWorktree(base) {
|
|
11630
11695
|
return diffTrees(this.treeAt(base), new Map(this.worktree));
|
|
11631
11696
|
}
|
|
11697
|
+
diffPaths(base, tip) {
|
|
11698
|
+
for (const ref of [base, tip]) if (this.resolveRef(ref) === null) throw new Error(`Invalid ref: ${ref}`);
|
|
11699
|
+
return diffTrees(this.treeAt(base), this.treeAt(tip)).map((e) => e.path);
|
|
11700
|
+
}
|
|
11632
11701
|
/** The worktree content of `path`, or `undefined` when absent. A LIVE read — never a snapshot. */
|
|
11633
11702
|
readFile(path) {
|
|
11634
11703
|
return this.worktree.get(path);
|
|
@@ -11927,7 +11996,8 @@ const makeGitReaderOps = (repo, root) => ({
|
|
|
11927
11996
|
gitCommonDir: () => succeed$6(`${root}/.git`),
|
|
11928
11997
|
commitHistory: (base, head) => succeed$6(repo.commitHistory(base, head)),
|
|
11929
11998
|
subjectHistory: (pageSize, skip, head) => succeed$6(repo.subjectHistory(pageSize, skip, head)),
|
|
11930
|
-
changedPaths: (base) => succeed$6(repo.changedPathsWorktree(base))
|
|
11999
|
+
changedPaths: (base) => succeed$6(repo.changedPathsWorktree(base)),
|
|
12000
|
+
diffPaths: (base, tip) => tryCatch(() => repo.diffPaths(base, tip))
|
|
11931
12001
|
});
|
|
11932
12002
|
const failOn = (output) => {
|
|
11933
12003
|
if (output !== void 0) throw new Error(output);
|
|
@@ -12064,6 +12134,13 @@ const missingToolCommands = {
|
|
|
12064
12134
|
};
|
|
12065
12135
|
/** The fine-grained `GitService` layer alone — for unit tests that need only git. */
|
|
12066
12136
|
const gitTestLayer = (repo, root = "/repo") => succeed$1(GitService, fakeGitOperations(repo, root));
|
|
12137
|
+
/**
|
|
12138
|
+
* The one sanctioned leak of the real `process.env` into the in-memory `Host`:
|
|
12139
|
+
* the runner's `GTD_FLAG_*` state, so a flag-on `npm test` state exercises the
|
|
12140
|
+
* flag in memory as it does live. A test asserting one flag side sets the flag
|
|
12141
|
+
* itself; explicit env wins.
|
|
12142
|
+
*/
|
|
12143
|
+
const ambientFlagEnv = (env = process.env) => Object.fromEntries(Object.entries(env).filter((e) => e[0].startsWith("GTD_FLAG_") && e[1] !== void 0));
|
|
12067
12144
|
/** Every layer `makeProgram` needs — `CommandRequirements`'s return type here IS the guarantee: a new port added there fails this function's typecheck instead of silently under-providing. */
|
|
12068
12145
|
function testLayers(repo, opts = {}) {
|
|
12069
12146
|
const root = opts.root ?? "/repo";
|
|
@@ -12071,7 +12148,10 @@ function testLayers(repo, opts = {}) {
|
|
|
12071
12148
|
return mergeAll$1(gitTestLayer(repo, root), succeed$1(Workspace, makeInMemoryWorkspaceOps(repo, root)), Host.layer({
|
|
12072
12149
|
root,
|
|
12073
12150
|
home,
|
|
12074
|
-
env:
|
|
12151
|
+
env: {
|
|
12152
|
+
...ambientFlagEnv(),
|
|
12153
|
+
...opts.env
|
|
12154
|
+
},
|
|
12075
12155
|
realPath: () => succeed$6(root)
|
|
12076
12156
|
}), ConfigService.Live, makeInMemoryConfigDiscovery(repo, root), Narrator.layer(opts.narrate ?? (() => {}), opts.verbose ?? true), succeed$1(ToolCommands, {
|
|
12077
12157
|
...missingToolCommands,
|
|
@@ -13395,7 +13475,7 @@ const PATHOLOGICAL_PATHS = [
|
|
|
13395
13475
|
}
|
|
13396
13476
|
];
|
|
13397
13477
|
/**
|
|
13398
|
-
* Exercise
|
|
13478
|
+
* Exercise every `GitOperations` methods identically against `makeTier()` —
|
|
13399
13479
|
* called once per tier by `src/platform/Git.test.ts`. A capability-gated group
|
|
13400
13480
|
* (`t.capabilities.X`) is skipped, not faked, on a tier that can't support it.
|
|
13401
13481
|
*/
|
|
@@ -13430,7 +13510,8 @@ const runGitServiceContract = (makeTier) => {
|
|
|
13430
13510
|
g.subjectHistory(10, 0),
|
|
13431
13511
|
g.readFileAtRef("HEAD", "second.txt"),
|
|
13432
13512
|
g.changedPaths(),
|
|
13433
|
-
g.changedPaths("HEAD~1")
|
|
13513
|
+
g.changedPaths("HEAD~1"),
|
|
13514
|
+
g.diffPaths("HEAD~1", "HEAD")
|
|
13434
13515
|
]));
|
|
13435
13516
|
expect(await t.observe.snapshot()).toBe(before);
|
|
13436
13517
|
await runGit(t, (g) => g.updateRef("refs/gtd/marker", t.observe.resolveRef("HEAD")));
|
|
@@ -13835,6 +13916,71 @@ const runGitServiceContract = (makeTier) => {
|
|
|
13835
13916
|
expect(isFailure(await runGitExit(t, (g) => g.readFileAtRef("HEAD", "nonexistent.txt")))).toBe(true);
|
|
13836
13917
|
});
|
|
13837
13918
|
});
|
|
13919
|
+
describe("diffPaths", () => {
|
|
13920
|
+
const sorted = (paths) => [...paths].sort();
|
|
13921
|
+
it("reports added, modified and deleted paths between two commits", async () => {
|
|
13922
|
+
t.seed.commit("chore: seed", {
|
|
13923
|
+
"mod.txt": "one",
|
|
13924
|
+
"del.txt": "bye"
|
|
13925
|
+
});
|
|
13926
|
+
const base = t.observe.resolveRef("HEAD");
|
|
13927
|
+
t.seed.writeFile("mod.txt", "two");
|
|
13928
|
+
t.seed.stageAll();
|
|
13929
|
+
t.seed.commitDeletion("del.txt", "chore: delete");
|
|
13930
|
+
t.seed.commit("feat: add", {
|
|
13931
|
+
"add.txt": "new",
|
|
13932
|
+
"mod.txt": "two"
|
|
13933
|
+
});
|
|
13934
|
+
const tip = t.observe.resolveRef("HEAD");
|
|
13935
|
+
const paths = await runGit(t, (g) => g.diffPaths(base, tip));
|
|
13936
|
+
expect(sorted(paths)).toEqual([
|
|
13937
|
+
"add.txt",
|
|
13938
|
+
"del.txt",
|
|
13939
|
+
"mod.txt"
|
|
13940
|
+
]);
|
|
13941
|
+
});
|
|
13942
|
+
it("reports both paths of a rename", async () => {
|
|
13943
|
+
t.seed.commit("chore: seed", { "old.txt": "same content unique-marker\n" });
|
|
13944
|
+
const base = t.observe.resolveRef("HEAD");
|
|
13945
|
+
t.seed.deleteFile("old.txt");
|
|
13946
|
+
t.seed.commit("refactor: rename", { "new.txt": "same content unique-marker\n" });
|
|
13947
|
+
const tip = t.observe.resolveRef("HEAD");
|
|
13948
|
+
expect(sorted(await runGit(t, (g) => g.diffPaths(base, tip)))).toEqual(["new.txt", "old.txt"]);
|
|
13949
|
+
});
|
|
13950
|
+
it("omits a file changed then restored between the endpoints", async () => {
|
|
13951
|
+
t.seed.commit("chore: seed", { "flip.txt": "one" });
|
|
13952
|
+
const base = t.observe.resolveRef("HEAD");
|
|
13953
|
+
t.seed.commit("chore: change", { "flip.txt": "two" });
|
|
13954
|
+
t.seed.commit("chore: restore", { "flip.txt": "one" });
|
|
13955
|
+
const tip = t.observe.resolveRef("HEAD");
|
|
13956
|
+
expect(await runGit(t, (g) => g.diffPaths(base, tip))).toEqual([]);
|
|
13957
|
+
});
|
|
13958
|
+
it("never reports staged, unstaged or untracked changes", async () => {
|
|
13959
|
+
t.seed.commit("chore: seed", {
|
|
13960
|
+
"a.txt": "a",
|
|
13961
|
+
"b.txt": "b"
|
|
13962
|
+
});
|
|
13963
|
+
const base = t.observe.resolveRef("HEAD");
|
|
13964
|
+
t.seed.writeFile("a.txt", "staged");
|
|
13965
|
+
t.seed.stageAll();
|
|
13966
|
+
t.seed.writeFile("b.txt", "unstaged");
|
|
13967
|
+
t.seed.writeFile("untracked.txt", "u");
|
|
13968
|
+
expect(await runGit(t, (g) => g.diffPaths(base, "HEAD"))).toEqual([]);
|
|
13969
|
+
});
|
|
13970
|
+
it("is [] for diffPaths(x, x)", async () => {
|
|
13971
|
+
const head = t.observe.resolveRef("HEAD");
|
|
13972
|
+
expect(await runGit(t, (g) => g.diffPaths(head, head))).toEqual([]);
|
|
13973
|
+
});
|
|
13974
|
+
for (const { label, path } of PATHOLOGICAL_PATHS) it(`reports a path containing ${label}, verbatim`, async () => {
|
|
13975
|
+
const base = t.observe.resolveRef("HEAD");
|
|
13976
|
+
t.seed.commit("feat: pathological", { [path]: "content" });
|
|
13977
|
+
expect(await runGit(t, (g) => g.diffPaths(base, "HEAD"))).toEqual([path]);
|
|
13978
|
+
});
|
|
13979
|
+
it("fails when either side does not resolve", async () => {
|
|
13980
|
+
expect(isFailure(await runGitExit(t, (g) => g.diffPaths("totally-invalid-ref-xyz", "HEAD")))).toBe(true);
|
|
13981
|
+
expect(isFailure(await runGitExit(t, (g) => g.diffPaths("HEAD", "totally-invalid-ref-xyz")))).toBe(true);
|
|
13982
|
+
});
|
|
13983
|
+
});
|
|
13838
13984
|
describe("changedPaths", () => {
|
|
13839
13985
|
it("unions a tracked modification, a tracked deletion, and an untracked addition, deduplicated by path", async () => {
|
|
13840
13986
|
t.seed.commit("chore: seed", {
|
package/dist/wire.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as EXIT_SIGTERM, D as
|
|
2
|
-
export { ACCESS_REFUSAL, BEAT_KINDS, DEMAND_BRIEFING, EXIT_CODES, EXIT_OK, EXIT_RUNTIME_ERROR, EXIT_SIGINT, EXIT_SIGTERM, 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 { C as EXIT_SIGTERM, D as UNATTRIBUTED_MODEL, E as ACCESS_REFUSAL, O as WIRE_SCHEMA, S as EXIT_SIGINT, T as EXIT_USAGE_ERROR, _ as demandOf, a as landFields, b as EXIT_OK, c as renderLandJson, d as renderBeatPlain, f as beatDocument, g as beatKindOf, h as BEAT_KINDS, i as judgeJson, k as renderFormat, l as renderLandPlain, m as statusOf, n as resolveSession, o as landProseText, p as renderBeatJson, r as selectPath, s as noopText, t as globMatches, u as DEMAND_BRIEFING, v as stallDiagnosis, w as EXIT_STRAYS_FOUND, x as EXIT_RUNTIME_ERROR, y as EXIT_CODES } from "./chunks/wire-BY1S-obW.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, resolveSession, selectPath, stallDiagnosis, statusOf };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { answered, changes, changesSince, glob, hasThreadFor, head, human, judge, numeric, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, start, vars } from "../flows.mjs";
|
|
2
2
|
import { review as review$1 } from "../workflow-kit.mjs";
|
|
3
|
-
import { $ as unwindFailedMessage, A as architectureDecomposePrompt, B as designSystem, C as fixRisks, D as agentWithSkills, E as reviewing, F as buildReviewFixNitsPrompt, G as packagesItemBuildingPrompt, I as buildReviewTriageMessage, J as reviewEditsCapture, K as packagesItemFixSuitePrompt, L as builderSystem, M as buildReviewAnswerQuestionsPrompt, N as buildReviewAwaitAfterDeferMessage, O as architectSystem, P as buildReviewCollectingPrompt, Q as startGateBlockedMessage, R as deferNotesScript, S as fixCheck, T as reviewMissing, V as designTriagePrompt, W as idleMessage, X as reviewNotesCapture, Z as reviewerSystem, _ as FEEDBACK, b as awaitReview, c as refusePendingHandOff, d as defaults, et as unwindFailure, f as envDefaults, g as ARCHITECTURE, h as healthy, i as skills, j as architectureGateAnswerMessage, k as architectureAuthorPrompt, l as tail, m as gate, n as summary, o as closeReview, q as reviewEditNotesCapture, r as access, t as steering, u as shipped, v as REQUIREMENTS, w as planner, x as coder, y as REVIEW, z as designGateAnswerMessage } from "../chunks/reserved-
|
|
3
|
+
import { $ as unwindFailedMessage, A as architectureDecomposePrompt, B as designSystem, C as fixRisks, D as agentWithSkills, E as reviewing, F as buildReviewFixNitsPrompt, G as packagesItemBuildingPrompt, I as buildReviewTriageMessage, J as reviewEditsCapture, K as packagesItemFixSuitePrompt, L as builderSystem, M as buildReviewAnswerQuestionsPrompt, N as buildReviewAwaitAfterDeferMessage, O as architectSystem, P as buildReviewCollectingPrompt, Q as startGateBlockedMessage, R as deferNotesScript, S as fixCheck, T as reviewMissing, V as designTriagePrompt, W as idleMessage, X as reviewNotesCapture, Z as reviewerSystem, _ as FEEDBACK, b as awaitReview, c as refusePendingHandOff, d as defaults, et as unwindFailure, f as envDefaults, g as ARCHITECTURE, h as healthy, i as skills, j as architectureGateAnswerMessage, k as architectureAuthorPrompt, l as tail, m as gate, n as summary, o as closeReview, q as reviewEditNotesCapture, r as access, t as steering, u as shipped, v as REQUIREMENTS, w as planner, x as coder, y as REVIEW, z as designGateAnswerMessage } from "../chunks/reserved-BL9krPao.mjs";
|
|
4
4
|
//#region src/workflows/feature/steps.ts
|
|
5
5
|
/** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
|
|
6
6
|
const triage = (base) => agentWithSkills("triage", designTriagePrompt(base), {
|
package/dist/workflows/fix.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
|
|
2
2
|
import { d as reviewNotes, f as reviewRisks } from "../chunks/steering-DVILanVf.mjs";
|
|
3
|
-
import { D as agentWithSkills, H as finisherSystem, U as fixReviewNotesPrompt, a as REVIEW_ORIG, d as defaults, f as envDefaults, i as skills, n as summary, p as baseline, r as access, s as handOffTail, t as steering, u as shipped, x as coder, y as REVIEW } from "../chunks/reserved-
|
|
3
|
+
import { D as agentWithSkills, H as finisherSystem, U as fixReviewNotesPrompt, a as REVIEW_ORIG, d as defaults, f as envDefaults, i as skills, n as summary, p as baseline, r as access, s as handOffTail, t as steering, u as shipped, x as coder, y as REVIEW } from "../chunks/reserved-BL9krPao.mjs";
|
|
4
4
|
//#region src/workflows/fix/fix.ts
|
|
5
5
|
/**
|
|
6
6
|
* Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { start } from "../flows.mjs";
|
|
2
|
-
import { Y as reviewGateBlockedMessage, c as refusePendingHandOff, d as defaults, f as envDefaults, i as skills, m as gate, n as summary, r as access, s as handOffTail, t as steering, u as shipped } from "../chunks/reserved-
|
|
2
|
+
import { Y as reviewGateBlockedMessage, c as refusePendingHandOff, d as defaults, f as envDefaults, i as skills, m as gate, n as summary, r as access, s as handOffTail, t as steering, u as shipped } from "../chunks/reserved-BL9krPao.mjs";
|
|
3
3
|
//#region src/workflows/review/review.ts
|
|
4
4
|
/**
|
|
5
5
|
* Pure review of everything since `reviewBase`: a green gate, the quality
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmelab/gtd",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "27.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
|
|
6
6
|
"bin": {
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"postbuild": "node scripts/assert-no-test-doubles.mjs",
|
|
107
107
|
"watch": "tsdown --watch",
|
|
108
108
|
"bench:replay": "node scripts/bench-replay.mjs",
|
|
109
|
-
"test": "
|
|
109
|
+
"test": "jiti scripts/test-states.main.ts",
|
|
110
110
|
"test:unit": "vitest run --project unit",
|
|
111
111
|
"test:workflows": "vitest run --project workflows",
|
|
112
112
|
"test:watch": "vitest",
|
package/src/cli/Cli.test.ts
CHANGED
|
@@ -981,9 +981,60 @@ describe("parseArgv — gtd history", () => {
|
|
|
981
981
|
})
|
|
982
982
|
})
|
|
983
983
|
|
|
984
|
+
describe("parseArgv — gtd strays", () => {
|
|
985
|
+
const strays = (...args: string[]): CliPlan => parseArgv(["node", "gtd.js", "strays", ...args])
|
|
986
|
+
|
|
987
|
+
it("splits the range at '..' and carries the globs", () => {
|
|
988
|
+
const plan = strays("main..feature", "src/**", "*.md")
|
|
989
|
+
expect(plan).toMatchObject({
|
|
990
|
+
kind: "command",
|
|
991
|
+
command: { kind: "strays", base: "main", tip: "feature", globs: ["src/**", "*.md"] },
|
|
992
|
+
})
|
|
993
|
+
})
|
|
994
|
+
|
|
995
|
+
it("an empty side becomes HEAD; no globs is an empty list", () => {
|
|
996
|
+
expect(strays("main..")).toMatchObject({
|
|
997
|
+
command: { base: "main", tip: "HEAD", globs: [] },
|
|
998
|
+
})
|
|
999
|
+
expect(strays("..main")).toMatchObject({ command: { base: "HEAD", tip: "main" } })
|
|
1000
|
+
expect(strays("..")).toMatchObject({ command: { base: "HEAD", tip: "HEAD" } })
|
|
1001
|
+
})
|
|
1002
|
+
|
|
1003
|
+
it("a missing range, a range without '..', a '...' range and a second '..' are usage errors", () => {
|
|
1004
|
+
for (const args of [[], ["main"], ["main...feature"], ["a..b..c"]]) {
|
|
1005
|
+
expect(strays(...args).kind).toBe("usage")
|
|
1006
|
+
}
|
|
1007
|
+
const plan = strays("main")
|
|
1008
|
+
if (plan.kind === "usage") expect(plan.message).toContain("<base>..<tip>")
|
|
1009
|
+
})
|
|
1010
|
+
|
|
1011
|
+
it("--json is in scope: bare and selected", () => {
|
|
1012
|
+
expect(strays("a..b", "--json")).toMatchObject({ kind: "command", json: { kind: "document" } })
|
|
1013
|
+
expect(strays("a..b", "--json=0")).toMatchObject({
|
|
1014
|
+
kind: "command",
|
|
1015
|
+
json: { kind: "select", path: "0" },
|
|
1016
|
+
})
|
|
1017
|
+
})
|
|
1018
|
+
|
|
1019
|
+
it("--json's scope error and help name strays", () => {
|
|
1020
|
+
const plan = parseArgv(["node", "gtd.js", "lsp", "--json"])
|
|
1021
|
+
if (plan.kind === "usage") expect(plan.message).toContain("`gtd strays`")
|
|
1022
|
+
expect(renderHelp()).toContain("strays <range> [glob...]")
|
|
1023
|
+
expect(renderHelp()).toContain("gtd strays only")
|
|
1024
|
+
})
|
|
1025
|
+
})
|
|
1026
|
+
|
|
984
1027
|
describe("standaloneKinds / needsOf", () => {
|
|
985
1028
|
it("pins the standalone kinds", () => {
|
|
986
|
-
expect(standaloneKinds()).toEqual([
|
|
1029
|
+
expect(standaloneKinds()).toEqual([
|
|
1030
|
+
"lsp",
|
|
1031
|
+
"check",
|
|
1032
|
+
"uncheck",
|
|
1033
|
+
"install",
|
|
1034
|
+
"loop",
|
|
1035
|
+
"judgeRun",
|
|
1036
|
+
"strays",
|
|
1037
|
+
])
|
|
987
1038
|
})
|
|
988
1039
|
|
|
989
1040
|
it("needsOf matches none/fs for the standalone kinds and state for everything else", () => {
|
|
@@ -993,6 +1044,7 @@ describe("standaloneKinds / needsOf", () => {
|
|
|
993
1044
|
expect(needsOf("install")).toBe("none")
|
|
994
1045
|
expect(needsOf("loop")).toBe("none")
|
|
995
1046
|
expect(needsOf("judgeRun")).toBe("none")
|
|
1047
|
+
expect(needsOf("strays")).toBe("none")
|
|
996
1048
|
for (const kind of [
|
|
997
1049
|
"land",
|
|
998
1050
|
"start",
|
package/src/cli/Cli.ts
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
type JsonMode,
|
|
30
30
|
} from "./Program.js"
|
|
31
31
|
import type { ToolCommands } from "./ToolCommands.js"
|
|
32
|
-
import {
|
|
32
|
+
import { EXIT_RUNTIME_ERROR, EXIT_USAGE_ERROR } from "../wire/index.js"
|
|
33
33
|
|
|
34
34
|
export type { Command, CommandRequirements } from "./Program.js"
|
|
35
35
|
export { needsOf, standaloneKinds } from "./Program.js"
|
|
@@ -96,15 +96,16 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
96
96
|
kind === "judgeAnswer" ||
|
|
97
97
|
kind === "doors" ||
|
|
98
98
|
kind === "historyList" ||
|
|
99
|
-
kind === "historyShow"
|
|
99
|
+
kind === "historyShow" ||
|
|
100
|
+
kind === "strays",
|
|
100
101
|
decode: ([raw]) => Either.right(raw ?? ""),
|
|
101
102
|
scopeError:
|
|
102
|
-
"gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer`/`gtd doors`/`gtd history list`/`gtd history show` — " +
|
|
103
|
+
"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` — " +
|
|
103
104
|
"every other command prints plain text; see `gtd install` for the driver protocol briefing",
|
|
104
105
|
valueHint: "<path>",
|
|
105
106
|
help: [
|
|
106
107
|
"(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd history",
|
|
107
|
-
"list/gtd history show only) output structured JSON. Bare --json prints the whole document;",
|
|
108
|
+
"list/gtd history show/gtd strays only) output structured JSON. Bare --json prints the whole document;",
|
|
108
109
|
"--json=<path> (a dotted key path into that document, e.g.",
|
|
109
110
|
"kind, content, session.id) prints just that value: a",
|
|
110
111
|
"scalar raw and unquoted, a boolean as true/false, a list",
|
|
@@ -667,6 +668,25 @@ const COMMAND_ROWS: readonly CommandRow[] = [
|
|
|
667
668
|
"every question, with nothing on stdout",
|
|
668
669
|
],
|
|
669
670
|
},
|
|
671
|
+
{
|
|
672
|
+
token: "strays",
|
|
673
|
+
kind: "strays",
|
|
674
|
+
arity: { name: "range", variadic: "glob" },
|
|
675
|
+
details: [
|
|
676
|
+
"List the files <range> (<base>..<tip>, an empty side is HEAD)",
|
|
677
|
+
"touched in its committed net diff that match none of the",
|
|
678
|
+
"globs. Anchored at the repo root: * stays within one path",
|
|
679
|
+
"segment, ** crosses them, everything else is literal — quote",
|
|
680
|
+
"globs in the shell. stdout: a sorted JSON array, [] when",
|
|
681
|
+
"clean; stderr: a summary, plus one stray: line per path with",
|
|
682
|
+
"--verbose. --json=<n> picks one entry, nothing past the end;",
|
|
683
|
+
"a symlink is a path like any other, never followed; a",
|
|
684
|
+
"submodule counts as its path, whatever diff.ignoreSubmodules",
|
|
685
|
+
"or .gitmodules ignore say. Runs in any git repository, no",
|
|
686
|
+
"process needed. Exits 10 when",
|
|
687
|
+
"strays were found, 1 on an unknown revision, 2 on a bad range",
|
|
688
|
+
],
|
|
689
|
+
},
|
|
670
690
|
{
|
|
671
691
|
token: "history list",
|
|
672
692
|
kind: "historyList",
|
|
@@ -1228,6 +1248,28 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1228
1248
|
}
|
|
1229
1249
|
}
|
|
1230
1250
|
|
|
1251
|
+
if (kind === "strays") {
|
|
1252
|
+
const [range, ...globs] = restPositionals as [string, ...string[]]
|
|
1253
|
+
const at = range.indexOf("..")
|
|
1254
|
+
if (at < 0 || range.includes("...") || range.indexOf("..", at + 1) >= 0) {
|
|
1255
|
+
return usagePlan(
|
|
1256
|
+
`gtd strays: <range> must be <base>..<tip> (an empty side is HEAD) — got '${range}'`,
|
|
1257
|
+
jsonSeen,
|
|
1258
|
+
)
|
|
1259
|
+
}
|
|
1260
|
+
return {
|
|
1261
|
+
kind: "command",
|
|
1262
|
+
command: {
|
|
1263
|
+
kind: "strays",
|
|
1264
|
+
base: range.slice(0, at) || "HEAD",
|
|
1265
|
+
tip: range.slice(at + 2) || "HEAD",
|
|
1266
|
+
globs,
|
|
1267
|
+
},
|
|
1268
|
+
json,
|
|
1269
|
+
verbose,
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1231
1273
|
if (kind === "uncheck") {
|
|
1232
1274
|
return {
|
|
1233
1275
|
kind: "command",
|
|
@@ -1419,9 +1461,9 @@ export const runCli = (argv: readonly string[], io: CliIo): Effect.Effect<void,
|
|
|
1419
1461
|
// where the whole buffered artifact is known-complete and safe to hand
|
|
1420
1462
|
// to stdout (a failure discards the buffer instead — see
|
|
1421
1463
|
// `bufferedArtifactOut`).
|
|
1422
|
-
Effect.map(() => {
|
|
1464
|
+
Effect.map((code) => {
|
|
1423
1465
|
out.flush()
|
|
1424
|
-
io.exit(
|
|
1466
|
+
io.exit(code)
|
|
1425
1467
|
}),
|
|
1426
1468
|
Effect.provide(io.layers(plan.verbose)),
|
|
1427
1469
|
Effect.sandbox,
|
package/src/cli/Output.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
2
|
import { CommandRunner, GitService, Host, Narrator, Workspace } from "../platform/index.js"
|
|
3
|
-
import { selectPath } from "../wire/index.js"
|
|
3
|
+
import { selectPath, type EXIT_OK, type EXIT_STRAYS_FOUND } from "../wire/index.js"
|
|
4
4
|
import { ConfigDiscovery, ConfigService } from "../workflow/index.js"
|
|
5
5
|
import type { ToolCommands } from "./ToolCommands.js"
|
|
6
6
|
|
|
@@ -66,3 +66,5 @@ export type CommandRequirements =
|
|
|
66
66
|
| Narrator
|
|
67
67
|
| CommandRunner
|
|
68
68
|
| ToolCommands
|
|
69
|
+
|
|
70
|
+
export type SuccessExit = typeof EXIT_OK | typeof EXIT_STRAYS_FOUND
|
package/src/cli/Program.test.ts
CHANGED
|
@@ -1693,6 +1693,7 @@ describe("runCommand — refuses in a repository with no commits", () => {
|
|
|
1693
1693
|
historyList: { kind: "historyList" },
|
|
1694
1694
|
historyShow: { kind: "historyShow", ref: "x" },
|
|
1695
1695
|
historyFetch: { kind: "historyFetch", remote: "origin" },
|
|
1696
|
+
strays: { kind: "strays", base: "HEAD", tip: "HEAD", globs: [] },
|
|
1696
1697
|
}
|
|
1697
1698
|
|
|
1698
1699
|
const stateKinds = (Object.keys(commandFor) as Command["kind"][]).filter(
|