@pmelab/gtd 8.5.0 → 8.6.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/README.md +135 -5
- package/bin/gtd +183 -20
- package/dist/gtd.bundle.mjs +900 -308
- package/package.json +1 -1
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -191387,6 +191387,20 @@ const STEERING_FORMATS = /* @__PURE__ */ new Map([["qa", QA_FORMAT], ["review",
|
|
|
191387
191387
|
const builtInModeNames = () => [...STEERING_FORMATS.keys()];
|
|
191388
191388
|
/** The built-in `SteeringFormat` named `mode`, or `undefined` when `mode` names no built-in. */
|
|
191389
191389
|
const steeringFormatFor = (mode) => STEERING_FORMATS.get(mode);
|
|
191390
|
+
/**
|
|
191391
|
+
* The literal (unrendered) Eta template a workflow compiler seeds as a
|
|
191392
|
+
* built-in mode's own `validate:` command — quoted so a file path containing
|
|
191393
|
+
* spaces survives `renderModeCommand`'s interpolation.
|
|
191394
|
+
*/
|
|
191395
|
+
const seededValidateCommand = (mode) => `gtd check ${mode} '<%= it.file %>'`;
|
|
191396
|
+
/**
|
|
191397
|
+
* True when `command` is EXACTLY `mode`'s own seeded template string — a
|
|
191398
|
+
* literal comparison against the declared template, never a rendered
|
|
191399
|
+
* command, so recognition doesn't depend on which file the mode is applied
|
|
191400
|
+
* to. Lets a later mode resolver tell gtd's own seeding apart from a user's
|
|
191401
|
+
* hand-written override of the same mode.
|
|
191402
|
+
*/
|
|
191403
|
+
const isSeededValidateCommand = (mode, command) => command === seededValidateCommand(mode);
|
|
191390
191404
|
//#endregion
|
|
191391
191405
|
//#region src/PatternConfig.ts
|
|
191392
191406
|
/**
|
|
@@ -191460,11 +191474,13 @@ const steeringFormatFor = (mode) => STEERING_FORMATS.get(mode);
|
|
|
191460
191474
|
* `ConfigService` compiles through this module's `compileModesMap` and hands
|
|
191461
191475
|
* back in as `rcModes`, merged per half by `mergeModes`. Underneath BOTH layers
|
|
191462
191476
|
* this compiler seeds `src/SteeringFormats.ts`'s built-in registry names (`qa`/
|
|
191463
|
-
* `review`)
|
|
191464
|
-
*
|
|
191465
|
-
*
|
|
191466
|
-
*
|
|
191467
|
-
*
|
|
191477
|
+
* `review`) with that module's own `seededValidateCommand(name)` template as
|
|
191478
|
+
* their `validate:`, so every compiled definition's `modes` map always carries
|
|
191479
|
+
* a usable `validate:` command for them and a workflow never has to declare
|
|
191480
|
+
* them just to use gtd's own validators — a workflow (or `.gtdrc`) may still
|
|
191481
|
+
* override either half per name, and `src/SteeringFormats.ts`'s
|
|
191482
|
+
* `isSeededValidateCommand` lets the edge (`src/SteeringMode.ts`) tell gtd's
|
|
191483
|
+
* own seeding apart from a genuine user override.
|
|
191468
191484
|
*
|
|
191469
191485
|
* ## File references
|
|
191470
191486
|
*
|
|
@@ -192105,7 +192121,7 @@ const compileWorkflowConfig = (raw, configDir, rcModes, inlineFileRefs = true, f
|
|
|
192105
192121
|
const unknownTopKeys = Object.keys(raw).filter((k) => !KNOWN_TOP_KEYS.has(k));
|
|
192106
192122
|
if (unknownTopKeys.length > 0) errors.push(`unknown top-level key(s) ${unknownTopKeys.join(", ")}`);
|
|
192107
192123
|
const vars = compileVarsMap(raw.vars, errors);
|
|
192108
|
-
const seeded = Object.fromEntries(builtInModeNames().map((name) => [name, {}]));
|
|
192124
|
+
const seeded = Object.fromEntries(builtInModeNames().map((name) => [name, { validate: seededValidateCommand(name) }]));
|
|
192109
192125
|
const modes = mergeModes(mergeModes(seeded, compileModesMap(raw.modes, errors)), rcModes);
|
|
192110
192126
|
validateMachinesShape(raw.machines, errors);
|
|
192111
192127
|
const flattened = flattenMachines(raw, errors);
|
|
@@ -192761,13 +192777,15 @@ const makeGitImpl = (executor, root) => {
|
|
|
192761
192777
|
hasCommits: () => exec("git", "rev-parse", "--verify", "HEAD").pipe(map$4(() => true), catchAll(() => succeed$3(false))),
|
|
192762
192778
|
readFileAtRef: (ref, path) => exec("git", "show", `${ref}:${path}`),
|
|
192763
192779
|
changedPathsSince: (ref) => exec("git", "diff", "--name-status", ref, "HEAD").pipe(map$4(parseNameStatus)),
|
|
192764
|
-
changedPaths: () => gen(function* () {
|
|
192765
|
-
const nameStatusOut = yield* exec("git", "diff", "--name-status", "HEAD").pipe(catchIf((e) => !isIndexLockError(e), () => succeed$3("")));
|
|
192780
|
+
changedPaths: (base) => gen(function* () {
|
|
192781
|
+
const nameStatusOut = yield* exec("git", "diff", "--name-status", base ?? "HEAD").pipe(catchIf((e) => !isIndexLockError(e), () => succeed$3("")));
|
|
192766
192782
|
const trackedPaths = parseNameStatus(nameStatusOut);
|
|
192767
|
-
const
|
|
192783
|
+
const untrackedAll = (yield* exec("git", "ls-files", "--others", "--exclude-standard", "-z")).split("\0").filter((s) => s.length > 0).map((path) => ({
|
|
192768
192784
|
path,
|
|
192769
192785
|
status: "A"
|
|
192770
192786
|
}));
|
|
192787
|
+
const atBase = base === void 0 ? /* @__PURE__ */ new Set() : new Set((yield* exec("git", "ls-tree", "-r", "--name-only", "-z", base)).split("\0").filter((s) => s.length > 0));
|
|
192788
|
+
const untracked = untrackedAll.filter((entry) => !atBase.has(entry.path));
|
|
192771
192789
|
const seen = /* @__PURE__ */ new Set();
|
|
192772
192790
|
const all = [];
|
|
192773
192791
|
for (const entry of [...trackedPaths, ...untracked]) if (!seen.has(entry.path)) {
|
|
@@ -192780,8 +192798,8 @@ const makeGitImpl = (executor, root) => {
|
|
|
192780
192798
|
readRefOption: (ref) => exec("git", "rev-parse", "--verify", "--quiet", ref).pipe(map$4((s) => some(s.trim())), catchAll(() => succeed$3(none$4()))),
|
|
192781
192799
|
isAncestor: (a, b) => run(root, "git", "merge-base", "--is-ancestor", a, b).pipe(provide$2(succeed$2(CommandExecutor, executor)), map$4(() => true), catchAll(() => succeed$3(false))),
|
|
192782
192800
|
topLevel: () => exec("git", "rev-parse", "--show-toplevel").pipe(map$4((s) => s.trim())),
|
|
192783
|
-
commitHistory: (base) => {
|
|
192784
|
-
const range = base !== void 0 ? `${base}
|
|
192801
|
+
commitHistory: (base, head = "HEAD") => {
|
|
192802
|
+
const range = base !== void 0 ? `${base}..${head}` : head !== "HEAD" ? head : void 0;
|
|
192785
192803
|
const args = [
|
|
192786
192804
|
"git",
|
|
192787
192805
|
"log",
|
|
@@ -193491,8 +193509,225 @@ const restorability = (git, headHash, headMessage, tipHash) => gen(function* ()
|
|
|
193491
193509
|
reason: "HEAD has advanced past the squash — restoring would discard commits built on top of it"
|
|
193492
193510
|
};
|
|
193493
193511
|
});
|
|
193494
|
-
|
|
193495
|
-
|
|
193512
|
+
//#endregion
|
|
193513
|
+
//#region src/GitScript.ts
|
|
193514
|
+
/**
|
|
193515
|
+
* Bash-script builders mirroring `GitWriterOperations` (`src/Git.ts`) — one
|
|
193516
|
+
* function per writer method, each returning the equivalent bash as a
|
|
193517
|
+
* `string` instead of executing it. Scaffolding for a later change that will
|
|
193518
|
+
* emit these scripts rather than run git directly through the `Effect`
|
|
193519
|
+
* `CommandExecutor`; nothing calls this module yet.
|
|
193520
|
+
*
|
|
193521
|
+
* Pure, like `src/PatternMachine.ts`: no git, no filesystem, no `Effect`, no
|
|
193522
|
+
* IO of any kind. Every export is a plain, total function of its arguments.
|
|
193523
|
+
* `src/Git.ts`'s doc comments on `GitWriterOperations` are the source of
|
|
193524
|
+
* truth for each shape below; read them for WHY, not just WHAT.
|
|
193525
|
+
*/
|
|
193526
|
+
/**
|
|
193527
|
+
* POSIX single-quote escaping: wrap `value` in `'...'`, replacing every
|
|
193528
|
+
* embedded `'` with `'\''` (close quote, escaped literal quote, reopen
|
|
193529
|
+
* quote). The only place a raw string may reach a shell command in this
|
|
193530
|
+
* module — every builder below routes every interpolated value through this.
|
|
193531
|
+
*/
|
|
193532
|
+
const shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
193533
|
+
/**
|
|
193534
|
+
* `--allow-empty` mirrors `commitAllWithPrefix`/`commitAsIs`'s load-bearing
|
|
193535
|
+
* use in `src/Git.ts`: gtd's workflow commits may be empty on purpose. The
|
|
193536
|
+
* retry-without-hooks behavior there keys off the specific "empty git commit"
|
|
193537
|
+
* hook-rejection message, re-failing on anything else (`src/Git.ts`'s `run`
|
|
193538
|
+
* doc comment records the regression a blind retry-on-any-failure would
|
|
193539
|
+
* reintroduce: reporting success on a genuinely rejected commit — a lint
|
|
193540
|
+
* error, a rejected commit-msg hook). Bash CAN inspect the message: capture
|
|
193541
|
+
* the first attempt's combined output and discriminate on it in a `case`,
|
|
193542
|
+
* exactly like `isIndexLockError`/`error.message.includes(...)` do in
|
|
193543
|
+
* `src/Git.ts`, instead of retrying on the exit code alone.
|
|
193544
|
+
*/
|
|
193545
|
+
const commitAllowEmpty = (message) => {
|
|
193546
|
+
const m = shellQuote(message);
|
|
193547
|
+
return [
|
|
193548
|
+
`if ! out=$(git commit --allow-empty -m ${m} 2>&1); then`,
|
|
193549
|
+
` case "$out" in`,
|
|
193550
|
+
` *"empty git commit"*) git commit --allow-empty --no-verify -m ${m} ;;`,
|
|
193551
|
+
` *) printf '%s\\n' "$out" >&2; exit 1 ;;`,
|
|
193552
|
+
` esac`,
|
|
193553
|
+
`fi`
|
|
193554
|
+
].join("\n");
|
|
193555
|
+
};
|
|
193556
|
+
/**
|
|
193557
|
+
* `git add -A` then `git commit --allow-empty -m <message>`, retried without
|
|
193558
|
+
* hooks on the same hook rejection `commitAllowEmpty` guards against. Joined
|
|
193559
|
+
* with `&&` so a failing `git add` (e.g. a quoted path breaking it) never
|
|
193560
|
+
* reaches the commit — the same "lost files whose quoted paths broke a
|
|
193561
|
+
* swallowed `git add`" failure mode `src/Git.ts`'s `run` doc comment records.
|
|
193562
|
+
*/
|
|
193563
|
+
const commitAll = (message) => `git add -A &&\n${commitAllowEmpty(message)}`;
|
|
193564
|
+
/** `git commit --allow-empty -m <message>` — commits the index as-is, no implicit `git add`. */
|
|
193565
|
+
const commitAsIs = (message) => commitAllowEmpty(message);
|
|
193566
|
+
/** `git reset --soft <ref>` — HEAD moves; index and working tree untouched. */
|
|
193567
|
+
const softResetTo = (ref) => `git reset --soft ${shellQuote(ref)}`;
|
|
193568
|
+
/** `git reset --mixed <ref>` — HEAD and index move; working tree untouched. */
|
|
193569
|
+
const mixedResetTo = (ref) => `git reset --mixed ${shellQuote(ref)}`;
|
|
193570
|
+
/** `git reset --hard <ref>` — HEAD, index, AND working tree all move. */
|
|
193571
|
+
const hardResetTo = (ref) => `git reset --hard ${shellQuote(ref)}`;
|
|
193572
|
+
/**
|
|
193573
|
+
* `git add -A` then `git reset --hard HEAD` — discards every pending change,
|
|
193574
|
+
* tracked or untracked. Joined with `&&`: a failed stage must not reach the
|
|
193575
|
+
* hard reset, or untracked survivors remain — the exact outcome this builder
|
|
193576
|
+
* exists to avoid.
|
|
193577
|
+
*/
|
|
193578
|
+
const discardPending = () => `git add -A && git reset --hard HEAD`;
|
|
193579
|
+
/** `git update-ref <ref> <hash>` — point a repo-local ref at a commit. */
|
|
193580
|
+
const updateRef = (ref, hash) => `git update-ref ${shellQuote(ref)} ${shellQuote(hash)}`;
|
|
193581
|
+
/** `git update-ref -d <ref>` — idempotent: deleting a missing ref is already a no-op in real git. */
|
|
193582
|
+
const deleteRef = (ref) => `git update-ref -d ${shellQuote(ref)}`;
|
|
193583
|
+
/**
|
|
193584
|
+
* `git restore --staged --source=<source> -- <paths…>` — tolerant of the
|
|
193585
|
+
* SAME two cases `src/Git.ts`'s `Effect.catchIf` tolerates (a missing ref, or
|
|
193586
|
+
* no path matching at `source`), and NOT tolerant of `index.lock` contention,
|
|
193587
|
+
* which must still fail the script rather than being swallowed here (see
|
|
193588
|
+
* `src/Git.ts`'s `restoreStagedFrom` doc comment and `isIndexLockError`) — a
|
|
193589
|
+
* lock failure silently swallowed here would mean the `.gtd/` index pin
|
|
193590
|
+
* silently doesn't happen, leaking `.gtd/` plumbing into the surfaced review
|
|
193591
|
+
* diff, the exact thing the pin exists to prevent. Captures the command's
|
|
193592
|
+
* output and discriminates on it, mirroring `isIndexLockError`'s two
|
|
193593
|
+
* substrings, exactly like `commitAllowEmpty` above. An empty `paths` emits
|
|
193594
|
+
* nothing at all (a harmless, syntactically valid empty script), mirroring
|
|
193595
|
+
* the Effect implementation's own `paths.length === 0` skip.
|
|
193596
|
+
*/
|
|
193597
|
+
const restoreStagedFrom = (source, paths) => {
|
|
193598
|
+
if (paths.length === 0) return "";
|
|
193599
|
+
const pathArgs = paths.map(shellQuote).join(" ");
|
|
193600
|
+
return [
|
|
193601
|
+
`if ! out=$(git restore --staged --source=${shellQuote(source)} -- ${pathArgs} 2>&1); then`,
|
|
193602
|
+
` case "$out" in`,
|
|
193603
|
+
` *"index.lock"*|*"Another git process seems to be running"*) printf '%s\\n' "$out" >&2; exit 1 ;;`,
|
|
193604
|
+
` *) : ;;`,
|
|
193605
|
+
` esac`,
|
|
193606
|
+
`fi`
|
|
193607
|
+
].join("\n");
|
|
193608
|
+
};
|
|
193609
|
+
//#endregion
|
|
193610
|
+
//#region src/Emit.ts
|
|
193611
|
+
/**
|
|
193612
|
+
* Assembles the two scripts a `gtd next`-time decision hands the external
|
|
193613
|
+
* driver: `required` (everything that decides what lands in git) and
|
|
193614
|
+
* `optional` (presentation only — e.g. opening the review checkout window so
|
|
193615
|
+
* an editor's diff view has something to show; a driver that skips it loses
|
|
193616
|
+
* nothing but that view). Pure, like `src/GitScript.ts`: no git, no
|
|
193617
|
+
* filesystem, no `Effect`. Every input string (a `GitScript.ts` builder's
|
|
193618
|
+
* output, a steering mode's already-rendered `format:`/`validate:` command)
|
|
193619
|
+
* arrives already rendered — this module only concatenates and wraps.
|
|
193620
|
+
*
|
|
193621
|
+
* Both halves are meant to stand alone: a driver may run either, both, or
|
|
193622
|
+
* only `required`, and a person could paste either into a terminal. That's
|
|
193623
|
+
* why each carries its OWN copy of the precondition asserts and the retry
|
|
193624
|
+
* helper, rather than the two sharing one preamble a caller has to splice
|
|
193625
|
+
* together correctly.
|
|
193626
|
+
*/
|
|
193627
|
+
/**
|
|
193628
|
+
* The CLI resolves `expectedHead` (and a review window's `expectedHash`) at
|
|
193629
|
+
* DECIDE time; the script may RUN later, in a process that never shared
|
|
193630
|
+
* memory with the one that decided. This assertion closes that
|
|
193631
|
+
* time-of-check/time-of-use gap. A plain `[ ... ] || { ...; exit 1; }`
|
|
193632
|
+
* statement, not an `if`/`fi` block, so a line-oriented consumer can still
|
|
193633
|
+
* recognize it, and so it stays safe under `set -e`: in an OR list, `-e`
|
|
193634
|
+
* exempts every command except the LAST one, so the left-hand `[ ... ]`
|
|
193635
|
+
* failing here never trips `set -e` on its own — only the explicit `exit 1`
|
|
193636
|
+
* inside the right-hand group does. Exported (alongside `reviewWindowAssertion`)
|
|
193637
|
+
* solely so `src/testing/EmittedScriptRecognizer.ts` can re-derive and
|
|
193638
|
+
* string-compare this exact shape, the same "call the real builder, never
|
|
193639
|
+
* hand-copy its template" discipline it already applies to every
|
|
193640
|
+
* `GitScript.ts` builder.
|
|
193641
|
+
*
|
|
193642
|
+
* The probe is `git rev-parse --verify --quiet HEAD 2>/dev/null`, not the
|
|
193643
|
+
* bare `git rev-parse HEAD` a born repo's assertion could get away with —
|
|
193644
|
+
* load-bearing, not cosmetic: against an UNBORN HEAD (a repository with no
|
|
193645
|
+
* commits), the bare form prints the literal string `HEAD` to stdout (plus a
|
|
193646
|
+
* `fatal:` line to stderr) and exits 128, so `"$(git rev-parse HEAD)"` reads
|
|
193647
|
+
* as `"HEAD"`, never `""`, and an `expectedHead === ""` comparison could
|
|
193648
|
+
* never pass. `--verify --quiet` makes an unborn HEAD read back as an empty
|
|
193649
|
+
* string instead — the same idiom `reviewWindowAssertion` already uses for a
|
|
193650
|
+
* missing ref — so `[ "$(…)" = '' ]` is simply the unborn case of the same
|
|
193651
|
+
* comparison every born repo already runs; only the `printf` wording
|
|
193652
|
+
* branches on it, to avoid naming a hash that doesn't exist.
|
|
193653
|
+
*/
|
|
193654
|
+
const headAssertion = (expectedHead) => {
|
|
193655
|
+
const q = shellQuote(expectedHead);
|
|
193656
|
+
const probe = `[ "$(git rev-parse --verify --quiet HEAD 2>/dev/null)" = ${q} ] || `;
|
|
193657
|
+
return expectedHead === "" ? probe + "{ printf 'gtd: repository changed since this script was generated (expected a repository with no commits yet) — re-run gtd\\n' >&2; exit 1; }" : probe + `{ printf 'gtd: repository changed since this script was generated (expected HEAD %s) — re-run gtd\\n' ${q} >&2; exit 1; }`;
|
|
193658
|
+
};
|
|
193659
|
+
/**
|
|
193660
|
+
* Same shape as `headAssertion`, for the saved-head ref a review window
|
|
193661
|
+
* pins. `--verify --quiet ... 2>/dev/null` makes a MISSING ref read as an
|
|
193662
|
+
* empty string rather than erroring the substitution itself — the assertion
|
|
193663
|
+
* only needs to compare, never to fail outright before the `[ ... ]` runs.
|
|
193664
|
+
*/
|
|
193665
|
+
const reviewWindowAssertion = (ref, expectedHash) => {
|
|
193666
|
+
const refQ = shellQuote(ref);
|
|
193667
|
+
const hashQ = shellQuote(expectedHash);
|
|
193668
|
+
return `[ "$(git rev-parse --verify --quiet ${refQ} 2>/dev/null)" = ${hashQ} ] || { printf 'gtd: review window ref %s changed since this script was generated (expected %s) — re-run gtd\\n' ${refQ} ${hashQ} >&2; exit 1; }`;
|
|
193669
|
+
};
|
|
193670
|
+
/**
|
|
193671
|
+
* `gtd_retry` takes ONE already-`shellQuote`d command string and `eval`s it,
|
|
193672
|
+
* mirroring `src/Git.ts`'s `withIndexLockRetry`: retry ONLY on the same two
|
|
193673
|
+
* substrings `isIndexLockError` matches, with jittered exponential backoff
|
|
193674
|
+
* (~10ms doubling, capped at 6 TOTAL attempts — the initial try plus 5
|
|
193675
|
+
* retries), and propagate any other failure on the very first attempt. Output
|
|
193676
|
+
* is captured combined (`2>&1`) so the discrimination can inspect it exactly
|
|
193677
|
+
* like `commitAllowEmpty`/`restoreStagedFrom` in `src/GitScript.ts` do for
|
|
193678
|
+
* their own single-shot retries — this is the same technique, generalized
|
|
193679
|
+
* into a loop. `awk` (not bash, which has no fractional `sleep` builtin)
|
|
193680
|
+
* turns the millisecond backoff into the fractional-second argument `sleep`
|
|
193681
|
+
* needs; `-v` passes the values in rather than interpolating them into the
|
|
193682
|
+
* awk program text, so a variable's value can never be mistaken for awk
|
|
193683
|
+
* syntax.
|
|
193684
|
+
*/
|
|
193685
|
+
const RETRY_HELPER = [
|
|
193686
|
+
`gtd_retry() {`,
|
|
193687
|
+
` local cmd=$1 attempt=1 delay_ms=10 out total_ms jitter_ms`,
|
|
193688
|
+
` while true; do`,
|
|
193689
|
+
` if out=$(eval "$cmd" 2>&1); then`,
|
|
193690
|
+
` [ -n "$out" ] && printf '%s\\n' "$out"`,
|
|
193691
|
+
` return 0`,
|
|
193692
|
+
` fi`,
|
|
193693
|
+
` case "$out" in`,
|
|
193694
|
+
` *"index.lock"*|*"Another git process seems to be running"*) ;;`,
|
|
193695
|
+
` *) printf '%s\\n' "$out" >&2; return 1 ;;`,
|
|
193696
|
+
` esac`,
|
|
193697
|
+
` if [ "$attempt" -ge 6 ]; then`,
|
|
193698
|
+
` printf '%s\\n' "$out" >&2`,
|
|
193699
|
+
` return 1`,
|
|
193700
|
+
` fi`,
|
|
193701
|
+
` jitter_ms=$(( RANDOM % delay_ms + 1 ))`,
|
|
193702
|
+
` total_ms=$(( delay_ms + jitter_ms ))`,
|
|
193703
|
+
` sleep "$(awk -v ms="$total_ms" 'BEGIN { printf "%.3f", ms / 1000 }')"`,
|
|
193704
|
+
` delay_ms=$(( delay_ms * 2 ))`,
|
|
193705
|
+
` attempt=$(( attempt + 1 ))`,
|
|
193706
|
+
` done`,
|
|
193707
|
+
`}`
|
|
193708
|
+
].join("\n");
|
|
193709
|
+
const renderStep = (step) => step.kind === "gitWrite" ? `gtd_retry ${shellQuote(step.command)}` : step.command;
|
|
193710
|
+
const assembleScript = (preconditions, steps) => {
|
|
193711
|
+
if (steps.length === 0) return "";
|
|
193712
|
+
const sections = ["set -euo pipefail"];
|
|
193713
|
+
if (preconditions.expectedHead !== void 0) sections.push(headAssertion(preconditions.expectedHead));
|
|
193714
|
+
if (preconditions.reviewWindow) sections.push(reviewWindowAssertion(preconditions.reviewWindow.ref, preconditions.reviewWindow.expectedHash));
|
|
193715
|
+
if (steps.some((step) => step.kind === "gitWrite")) sections.push(RETRY_HELPER);
|
|
193716
|
+
sections.push(...steps.map(renderStep));
|
|
193717
|
+
return sections.join("\n\n");
|
|
193718
|
+
};
|
|
193719
|
+
/**
|
|
193720
|
+
* Build both halves from already-rendered command strings. `required`
|
|
193721
|
+
* defaults to `[]` (nothing to do), `optional` to `[]` (no review window to
|
|
193722
|
+
* open) — either omitted argument, or an explicit empty array, produces the
|
|
193723
|
+
* fixed empty-string result `EmittedScripts` documents: a driver checking
|
|
193724
|
+
* `if [ -n "$required" ]` (or the JS equivalent) never has to special-case a
|
|
193725
|
+
* bare preamble with no body.
|
|
193726
|
+
*/
|
|
193727
|
+
const emitScripts = (preconditions, required = [], optional = []) => ({
|
|
193728
|
+
required: assembleScript(preconditions, required),
|
|
193729
|
+
optional: assembleScript(preconditions, optional)
|
|
193730
|
+
});
|
|
193496
193731
|
//#endregion
|
|
193497
193732
|
//#region src/Edge.ts
|
|
193498
193733
|
/**
|
|
@@ -193537,6 +193772,17 @@ const clearRetainedHistory = (git) => git.deleteRef(HISTORY_REF);
|
|
|
193537
193772
|
* what `PatternMachine.step` consumes, despite having a single reader.
|
|
193538
193773
|
*/
|
|
193539
193774
|
const EMPTY_TREE$2 = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
193775
|
+
/**
|
|
193776
|
+
* The review checkout window's saved-head ref — duplicated from
|
|
193777
|
+
* `src/ReviewWindow.ts`'s own `REVIEW_HEAD_REF` rather than imported, because
|
|
193778
|
+
* `ReviewWindow.ts` already imports FROM this module (`currentRun`,
|
|
193779
|
+
* `reviewBaseFor`); importing it back here would be a real circular module
|
|
193780
|
+
* dependency. Mirrors `src/RetainedHistory.ts`'s own `HISTORY_REF` doc
|
|
193781
|
+
* comment on the same tradeoff. Read (never written) by `restAt`'s
|
|
193782
|
+
* window-aware branch and by the script preconditions below — this module
|
|
193783
|
+
* never opens or closes the window itself.
|
|
193784
|
+
*/
|
|
193785
|
+
const REVIEW_HEAD_REF$1 = "refs/worktree/gtd/review-head";
|
|
193540
193786
|
const subjectOf = (message) => (message.split("\n")[0] ?? "").trim();
|
|
193541
193787
|
const COST_TRAILER_PREFIX = "Gtd-Cost: ";
|
|
193542
193788
|
const COST_TRAILER_RE = /^Gtd-Cost:[ \t]*([0-9]+(?:\.[0-9]+)?)(?:[ \t]+(.+?))?[ \t]*$/gm;
|
|
@@ -193654,8 +193900,8 @@ const resolveRestFrom = (def, headSubject) => {
|
|
|
193654
193900
|
}
|
|
193655
193901
|
};
|
|
193656
193902
|
};
|
|
193657
|
-
/** The working tree's pending changes vs HEAD, as the pattern grammar's `{status, path}[]`. */
|
|
193658
|
-
const pendingChanges = (git) => git.changedPaths().pipe(map$4((entries) => entries.map((e) => ({
|
|
193903
|
+
/** The working tree's pending changes vs `base` (default HEAD), as the pattern grammar's `{status, path}[]`. */
|
|
193904
|
+
const pendingChanges = (git, base) => git.changedPaths(base).pipe(map$4((entries) => entries.map((e) => ({
|
|
193659
193905
|
status: normalizeStatus(e.status),
|
|
193660
193906
|
path: e.path
|
|
193661
193907
|
}))));
|
|
@@ -193696,9 +193942,16 @@ const parseEntryCommitOverrides = (processCommits) => {
|
|
|
193696
193942
|
* same shape a fresh rest at a squashed boundary has always had.
|
|
193697
193943
|
*
|
|
193698
193944
|
* `hasCommits` is supplied by the caller (already read once — see the module
|
|
193699
|
-
* doc comment's stage ordering) rather than re-read here.
|
|
193700
|
-
|
|
193701
|
-
|
|
193945
|
+
* doc comment's stage ordering) rather than re-read here. `head` — a resolved
|
|
193946
|
+
* hash, never a symbolic ref — overrides the literal `HEAD` the walk would
|
|
193947
|
+
* otherwise end at: `restAt`'s window-aware branch passes the review
|
|
193948
|
+
* checkout window's saved-head hash here while the window is open, so the
|
|
193949
|
+
* trace still covers the commits a real `git log` would miss with HEAD
|
|
193950
|
+
* rewound to the review base. `undefined` (every other caller) is today's
|
|
193951
|
+
* exact behavior — an explicit trailing ref is passed to `git.commitHistory`
|
|
193952
|
+
* only when `head` differs from the literal `"HEAD"` it already defaults to.
|
|
193953
|
+
*/
|
|
193954
|
+
const computeProcessRun = (git, def, hasCommits, head) => gen(function* () {
|
|
193702
193955
|
if (!hasCommits) return {
|
|
193703
193956
|
startHash: "",
|
|
193704
193957
|
startParentHash: EMPTY_TREE$2,
|
|
@@ -193708,7 +193961,7 @@ const computeProcessRun = (git, def, hasCommits) => gen(function* () {
|
|
|
193708
193961
|
entryVars: {}
|
|
193709
193962
|
};
|
|
193710
193963
|
const initialState = initialStateOf(def);
|
|
193711
|
-
const history = yield* git.commitHistory();
|
|
193964
|
+
const history = yield* git.commitHistory(void 0, head);
|
|
193712
193965
|
let i = history.length - 1;
|
|
193713
193966
|
while (i >= 0) {
|
|
193714
193967
|
const parsed = parseStateSubject(subjectOf(history[i].message));
|
|
@@ -193740,12 +193993,19 @@ const computeProcessRun = (git, def, hasCommits) => gen(function* () {
|
|
|
193740
193993
|
* since abandon IS the recovery command for that case) and the review
|
|
193741
193994
|
* window's re-arm (`openReviewWindow` stays graceful there too — see its own
|
|
193742
193995
|
* doc comment).
|
|
193996
|
+
*
|
|
193997
|
+
* Window-aware on exactly the same terms as `restAt`: while a review checkout
|
|
193998
|
+
* window is open, real HEAD is rewound to the review base, so walking the
|
|
193999
|
+
* trace from literal HEAD would see a process that has not started. Both
|
|
194000
|
+
* callers need the REAL head — abandon rewinds the reviewed branch tip, the
|
|
194001
|
+
* re-arm re-derives the review base — so the saved head is the walk's origin.
|
|
193743
194002
|
*/
|
|
193744
194003
|
const currentRun = gen(function* () {
|
|
193745
194004
|
const git = yield* GitService;
|
|
193746
194005
|
const config = yield* (yield* ConfigService).load;
|
|
193747
194006
|
const hasCommits = yield* git.hasCommits();
|
|
193748
|
-
|
|
194007
|
+
const windowHead = getOrUndefined(yield* git.readRefOption(REVIEW_HEAD_REF$1));
|
|
194008
|
+
return yield* computeProcessRun(git, config.workflow, hasCommits, windowHead);
|
|
193749
194009
|
});
|
|
193750
194010
|
/**
|
|
193751
194011
|
* PURE: the most-recent in-process turn commit that entered a `reviewBase`
|
|
@@ -193941,6 +194201,20 @@ const omitUndefined = (obj) => Object.fromEntries(Object.entries(obj).filter(([,
|
|
|
193941
194201
|
* window's saved head via `REVIEW_HEAD_REF`, or `undefined` for HEAD itself —
|
|
193942
194202
|
* see `currentRest`). Assembles every stage documented in the module doc
|
|
193943
194203
|
* comment, threading one `hasCommits()` read through stages 3/5/11.
|
|
194204
|
+
*
|
|
194205
|
+
* Window-aware ONLY at `ref === undefined` (`currentRest`'s own case): when
|
|
194206
|
+
* the review checkout window's saved-head ref (`REVIEW_HEAD_REF`) resolves,
|
|
194207
|
+
* its hash is used as the effective head for `lastCommitSubject` AND
|
|
194208
|
+
* `computeProcessRun`'s trace walk — real git HEAD has been rewound to the
|
|
194209
|
+
* review base while the window is open, so reading literal `HEAD` there
|
|
194210
|
+
* would resolve state/trace/memory against the wrong commit. An explicit
|
|
194211
|
+
* `ref` (`gtd visualize`'s own call pattern) never triggers this branch —
|
|
194212
|
+
* that path resolves exactly as it always has. Safe to land now because
|
|
194213
|
+
* every existing command closes the window (`program.ts`'s
|
|
194214
|
+
* `runInReviewWindowBracket`) before `restAt`/`currentRest` ever runs, so the
|
|
194215
|
+
* saved-head ref is already gone by the time this branch could fire in
|
|
194216
|
+
* production — see the two direct tests in `Edge.test.ts` for the only
|
|
194217
|
+
* coverage today.
|
|
193944
194218
|
*/
|
|
193945
194219
|
const restAt = (ref) => gen(function* () {
|
|
193946
194220
|
const git = yield* GitService;
|
|
@@ -193949,16 +194223,19 @@ const restAt = (ref) => gen(function* () {
|
|
|
193949
194223
|
const envVars = yield* EnvVars;
|
|
193950
194224
|
const def = config.workflow;
|
|
193951
194225
|
const hasCommits = yield* git.hasCommits();
|
|
193952
|
-
|
|
194226
|
+
let windowHead;
|
|
194227
|
+
if (ref === void 0) windowHead = getOrUndefined(yield* git.readRefOption(REVIEW_HEAD_REF$1));
|
|
194228
|
+
const effectiveRef = windowHead ?? ref;
|
|
194229
|
+
const headSubject = hasCommits ? yield* git.lastCommitSubject(effectiveRef) : "";
|
|
193953
194230
|
const resolution = resolveRestFrom(def, headSubject);
|
|
193954
194231
|
if (!resolution.ok) return yield* fail$2(resolution.error);
|
|
193955
194232
|
const resolved = resolution.rest;
|
|
193956
|
-
const run = yield* computeProcessRun(git, def, hasCommits);
|
|
194233
|
+
const run = yield* computeProcessRun(git, def, hasCommits, windowHead);
|
|
193957
194234
|
const vars = resolveVars(config.workflowVars, config.rcVars, run.entryVars, envVars.all);
|
|
193958
194235
|
const on = yield* renderOnEdgesOrFail(resolved.stateDef.on, vars);
|
|
193959
194236
|
const stepDef = withRenderedOn(def, resolved.state, on);
|
|
193960
194237
|
const reviewBase = reviewBaseFor(def, run);
|
|
193961
|
-
const changes = yield* pendingChanges(git);
|
|
194238
|
+
const changes = yield* pendingChanges(git, windowHead);
|
|
193962
194239
|
const context = yield* buildTemplateContext(git, templateRead(files), resolved.state, resolved.actor, run, vars, on, 0, void 0, reviewBase, hasCommits);
|
|
193963
194240
|
const memory = memoryKeyFor(config.stateScopes, resolved, run);
|
|
193964
194241
|
const hints = yield* renderHints(resolved.stateDef, context);
|
|
@@ -194035,10 +194312,13 @@ const renderRest = (rest) => gen(function* () {
|
|
|
194035
194312
|
const formatStepRefusal = (invoker, refusal) => refusal.reason === "out-of-turn" ? `gtd step ${invoker}: out of turn — "${refusal.state}" awaits ${refusal.awaits}` : `gtd step ${invoker}: no declared pattern matches the pending changes at "${refusal.state}" — declared patterns: ${refusal.patterns.length > 0 ? refusal.patterns.join(", ") : "(none)"}`;
|
|
194036
194313
|
/**
|
|
194037
194314
|
* Build the `TemplateContext` for rendering a DIFFERENT state than `rest`'s
|
|
194038
|
-
* own resting one, with a step's `cost`/`model` folded in
|
|
194039
|
-
*
|
|
194040
|
-
* `TemplateContext`
|
|
194041
|
-
* the
|
|
194315
|
+
* own resting one, with a step's `cost`/`model` folded in. A squash's commit
|
|
194316
|
+
* template is the only thing that needs it (the commit branch never reads a
|
|
194317
|
+
* `TemplateContext` at all), and it is NOT interchangeable with `rest.context`:
|
|
194318
|
+
* that one is pinned to the resting state and carries `cost: 0`, so rendering
|
|
194319
|
+
* `it.processCost` against it silently omits the squashing step's own
|
|
194320
|
+
* `--cost`. Exported for `program.ts`'s `stepAsActor`, which assembles the
|
|
194321
|
+
* same script by hand and must pick the same context.
|
|
194042
194322
|
*/
|
|
194043
194323
|
const contextAt = (rest, targetState, invoker, cost, model) => gen(function* () {
|
|
194044
194324
|
const git = yield* GitService;
|
|
@@ -194086,6 +194366,119 @@ const executeDecision = (git, run, decision, context, cost, model) => gen(functi
|
|
|
194086
194366
|
}
|
|
194087
194367
|
});
|
|
194088
194368
|
/**
|
|
194369
|
+
* Render a `"commit"`/`"squash"` decision as the `EmitStep`s the external
|
|
194370
|
+
* driver would run to produce the SAME effect `executeDecision` performs
|
|
194371
|
+
* directly — same switch, same subjects, same trailers, same ordering. Every
|
|
194372
|
+
* git call here is a READ (`git.resolveRef("HEAD")` for the squash branch's
|
|
194373
|
+
* pre-squash tip); nothing is written. `planStep`/`planEntry`'s script
|
|
194374
|
+
* assembly (`buildStepScripts`) and `Edge.test.ts`'s render/perform
|
|
194375
|
+
* equivalence tests are its two callers.
|
|
194376
|
+
*
|
|
194377
|
+
* The squash branch renders the commit-state template against the PENDING
|
|
194378
|
+
* tree — a render failure fails this Effect exactly like `executeDecision`'s
|
|
194379
|
+
* own (same message), touching no git. On success it emits, in order:
|
|
194380
|
+
* retain-history (`updateRef(HISTORY_REF, tip)`, OMITTED when `tip ===
|
|
194381
|
+
* run.startParentHash` — an empty process retains nothing, mirroring
|
|
194382
|
+
* `retainHistory`'s own no-op check, decided here since `tip` is already
|
|
194383
|
+
* known), soft-reset to the process start, commit-as-is with the rendered
|
|
194384
|
+
* message plus its `Gtd-History:` trailer, then discard-pending.
|
|
194385
|
+
*
|
|
194386
|
+
* The commit branch carries `executeDecision`'s one non-obvious case too: a
|
|
194387
|
+
* commit whose target is the workflow's INITIAL state, from a process that
|
|
194388
|
+
* retained nothing, is a no-op probe (`gtd --entry fix-precheck` against a
|
|
194389
|
+
* green suite is the shipped example) and must leave no trace at all. It
|
|
194390
|
+
* emits retain-history + a mixed reset to the process start instead of a
|
|
194391
|
+
* commit, so the entry commit and the probe collapse away together rather
|
|
194392
|
+
* than dirtying the log with a round trip to `idle`. Deciding it needs the
|
|
194393
|
+
* whole `rest` (its definition for the initial state, its pending changes for
|
|
194394
|
+
* the "retained nothing" test), which is why this takes a `Rest` rather than
|
|
194395
|
+
* the `ProcessRun` alone.
|
|
194396
|
+
*/
|
|
194397
|
+
const renderDecision = (git, rest, decision, context, cost, model) => gen(function* () {
|
|
194398
|
+
const run = rest.run;
|
|
194399
|
+
switch (decision.kind) {
|
|
194400
|
+
case "commit":
|
|
194401
|
+
if (parseStateSubject(decision.subject)?.state === initialStateOf(rest.def) && run.startParentHash !== EMPTY_TREE$2 && (yield* retainsNothing(git, run, rest.changes))) {
|
|
194402
|
+
const tip = yield* git.resolveRef("HEAD");
|
|
194403
|
+
const steps = [];
|
|
194404
|
+
if (tip !== run.startParentHash) steps.push({
|
|
194405
|
+
kind: "gitWrite",
|
|
194406
|
+
command: updateRef(HISTORY_REF, tip)
|
|
194407
|
+
});
|
|
194408
|
+
steps.push({
|
|
194409
|
+
kind: "gitWrite",
|
|
194410
|
+
command: mixedResetTo(run.startParentHash)
|
|
194411
|
+
});
|
|
194412
|
+
return steps;
|
|
194413
|
+
}
|
|
194414
|
+
return [{
|
|
194415
|
+
kind: "gitWrite",
|
|
194416
|
+
command: commitAll(withCostTrailer(decision.subject, cost, model))
|
|
194417
|
+
}];
|
|
194418
|
+
case "squash": {
|
|
194419
|
+
const message = yield* try_({
|
|
194420
|
+
try: () => renderStateTemplate(decision.template, context),
|
|
194421
|
+
catch: (e) => /* @__PURE__ */ new Error(`gtd: rendering the "${decision.state}" commit template failed — nothing was committed: ${e instanceof Error ? e.message : String(e)}`)
|
|
194422
|
+
});
|
|
194423
|
+
const tip = yield* git.resolveRef("HEAD");
|
|
194424
|
+
const steps = [];
|
|
194425
|
+
if (tip !== run.startParentHash) steps.push({
|
|
194426
|
+
kind: "gitWrite",
|
|
194427
|
+
command: updateRef(HISTORY_REF, tip)
|
|
194428
|
+
});
|
|
194429
|
+
steps.push({
|
|
194430
|
+
kind: "gitWrite",
|
|
194431
|
+
command: softResetTo(run.startParentHash)
|
|
194432
|
+
});
|
|
194433
|
+
steps.push({
|
|
194434
|
+
kind: "gitWrite",
|
|
194435
|
+
command: commitAsIs(withHistoryTrailer(message, tip))
|
|
194436
|
+
});
|
|
194437
|
+
steps.push({
|
|
194438
|
+
kind: "gitWrite",
|
|
194439
|
+
command: discardPending()
|
|
194440
|
+
});
|
|
194441
|
+
return steps;
|
|
194442
|
+
}
|
|
194443
|
+
}
|
|
194444
|
+
});
|
|
194445
|
+
/**
|
|
194446
|
+
* The `EmitPreconditions` a step's/entry's assembled scripts assert against:
|
|
194447
|
+
* `expectedHead` is the resolved HEAD hash the `Rest` snapshot was already
|
|
194448
|
+
* taken at — `rest.context.currentCommit`, passed straight through, `""`
|
|
194449
|
+
* included (its own convention for "no commits yet", which `headAssertion`
|
|
194450
|
+
* now understands natively). When `targetState` declares `reviewWindow: true`
|
|
194451
|
+
* AND a window is currently open (`REVIEW_HEAD_REF` resolves), the script also
|
|
194452
|
+
* pins the window's saved-head hash — a later run whose window has since
|
|
194453
|
+
* closed or moved must re-decide rather than trust a stale script. No window,
|
|
194454
|
+
* or a non-review-window target, carries no `reviewWindow` precondition at
|
|
194455
|
+
* all.
|
|
194456
|
+
*/
|
|
194457
|
+
const buildPreconditions = (git, rest, targetState) => gen(function* () {
|
|
194458
|
+
const expectedHead = rest.context.currentCommit;
|
|
194459
|
+
if (!isReviewWindowState(rest.def, targetState)) return { expectedHead };
|
|
194460
|
+
const windowHead = yield* git.readRefOption(REVIEW_HEAD_REF$1);
|
|
194461
|
+
if (isNone(windowHead)) return { expectedHead };
|
|
194462
|
+
return {
|
|
194463
|
+
expectedHead,
|
|
194464
|
+
reviewWindow: {
|
|
194465
|
+
ref: REVIEW_HEAD_REF$1,
|
|
194466
|
+
expectedHash: windowHead.value
|
|
194467
|
+
}
|
|
194468
|
+
};
|
|
194469
|
+
});
|
|
194470
|
+
/**
|
|
194471
|
+
* The `EmittedScripts` a `"commit"`/`"squash"` `StepPlan` carries alongside
|
|
194472
|
+
* `perform` — built from the SAME `renderDecision` output, but a render
|
|
194473
|
+
* failure collapses to an EMPTY script (`emitScripts` with no steps) instead
|
|
194474
|
+
* of failing this Effect: `perform` stays the one place a render failure is
|
|
194475
|
+
* ever reported to a caller, unchanged by this addition.
|
|
194476
|
+
*/
|
|
194477
|
+
const buildStepScripts = (git, rest, decision, context, cost, model) => gen(function* () {
|
|
194478
|
+
const targetState = decision.kind === "commit" ? decision.to : decision.state;
|
|
194479
|
+
return emitScripts(yield* buildPreconditions(git, rest, targetState), yield* renderDecision(git, rest, decision, context, cost, model).pipe(catchAll(() => succeed$3([]))));
|
|
194480
|
+
});
|
|
194481
|
+
/**
|
|
194089
194482
|
* Decide what invoking `invoker` at `rest` does (`PatternMachine.step` over
|
|
194090
194483
|
* `rest.stepDef`/`rest.changes`/`rest.run.trace`), and — for a `"commit"`/
|
|
194091
194484
|
* `"squash"` decision — build the `perform` Effect that actually writes it.
|
|
@@ -194099,19 +194492,19 @@ const executeDecision = (git, run, decision, context, cost, model) => gen(functi
|
|
|
194099
194492
|
* reads it) or a freshly built `contextAt` for a squash (which renders a
|
|
194100
194493
|
* DIFFERENT state's template with the step's cost folded in).
|
|
194101
194494
|
*/
|
|
194102
|
-
const planStep = (rest, invoker, opts = {}) => {
|
|
194495
|
+
const planStep = (rest, invoker, opts = {}) => gen(function* () {
|
|
194103
194496
|
const decision = step(rest.stepDef, rest.state, invoker, {
|
|
194104
194497
|
changes: rest.changes,
|
|
194105
194498
|
processTrace: rest.run.trace.map((entry) => entry.state)
|
|
194106
194499
|
});
|
|
194107
|
-
if (decision.kind === "refusal") return
|
|
194500
|
+
if (decision.kind === "refusal") return {
|
|
194108
194501
|
kind: "refusal",
|
|
194109
194502
|
message: formatStepRefusal(invoker, decision)
|
|
194110
|
-
}
|
|
194111
|
-
if (decision.kind === "noop") return
|
|
194503
|
+
};
|
|
194504
|
+
if (decision.kind === "noop") return {
|
|
194112
194505
|
kind: "noop",
|
|
194113
194506
|
state: decision.state
|
|
194114
|
-
}
|
|
194507
|
+
};
|
|
194115
194508
|
const { cost, model } = opts;
|
|
194116
194509
|
const perform = gen(function* () {
|
|
194117
194510
|
const git = yield* GitService;
|
|
@@ -194130,13 +194523,17 @@ const planStep = (rest, invoker, opts = {}) => {
|
|
|
194130
194523
|
const context = yield* contextAt(rest, decision.state, invoker, cost, model);
|
|
194131
194524
|
return yield* executeDecision(git, rest.run, decision, context, cost, model);
|
|
194132
194525
|
});
|
|
194133
|
-
|
|
194526
|
+
const git = yield* GitService;
|
|
194527
|
+
const scriptContext = decision.kind === "commit" ? rest.context : yield* contextAt(rest, decision.state, invoker, cost, model);
|
|
194528
|
+
const scripts = yield* buildStepScripts(git, rest, decision, scriptContext, cost, model);
|
|
194529
|
+
return {
|
|
194134
194530
|
kind: decision.kind,
|
|
194135
194531
|
state: rest.state,
|
|
194136
194532
|
decision,
|
|
194137
|
-
perform
|
|
194138
|
-
|
|
194139
|
-
};
|
|
194533
|
+
perform,
|
|
194534
|
+
scripts
|
|
194535
|
+
};
|
|
194536
|
+
});
|
|
194140
194537
|
/**
|
|
194141
194538
|
* `gtd step <actor> --entry <state>` (or its subcommand-less short form `gtd
|
|
194142
194539
|
* --entry <state>`): start a brand NEW process at `entry.state` — any
|
|
@@ -194220,18 +194617,24 @@ const planEntry = (rest, actor, entry) => gen(function* () {
|
|
|
194220
194617
|
...base !== void 0 ? { base } : {},
|
|
194221
194618
|
vars: varOverrides
|
|
194222
194619
|
});
|
|
194620
|
+
const perform = gen(function* () {
|
|
194621
|
+
yield* (yield* GitService).commitAllWithPrefix(message);
|
|
194622
|
+
return {
|
|
194623
|
+
kind: "entry",
|
|
194624
|
+
state: entryState,
|
|
194625
|
+
subject
|
|
194626
|
+
};
|
|
194627
|
+
});
|
|
194628
|
+
const scriptsGit = yield* GitService;
|
|
194223
194629
|
return {
|
|
194224
194630
|
kind: "entry",
|
|
194225
194631
|
state: entryState,
|
|
194226
194632
|
subject,
|
|
194227
|
-
perform
|
|
194228
|
-
|
|
194229
|
-
|
|
194230
|
-
|
|
194231
|
-
|
|
194232
|
-
subject
|
|
194233
|
-
};
|
|
194234
|
-
})
|
|
194633
|
+
perform,
|
|
194634
|
+
scripts: emitScripts(yield* buildPreconditions(scriptsGit, rest, entryState), [{
|
|
194635
|
+
kind: "gitWrite",
|
|
194636
|
+
command: commitAll(message)
|
|
194637
|
+
}])
|
|
194235
194638
|
};
|
|
194236
194639
|
});
|
|
194237
194640
|
//#endregion
|
|
@@ -194249,24 +194652,37 @@ const planEntry = (rest, actor, entry) => gen(function* () {
|
|
|
194249
194652
|
* `PatternMachine.StateDef`) — the pure engine never observes an open window,
|
|
194250
194653
|
* exactly as before.
|
|
194251
194654
|
*
|
|
194252
|
-
* Lifecycle — driven from the program edge (`src/program.ts`),
|
|
194253
|
-
*
|
|
194254
|
-
*
|
|
194255
|
-
*
|
|
194256
|
-
*
|
|
194257
|
-
*
|
|
194258
|
-
*
|
|
194259
|
-
*
|
|
194260
|
-
*
|
|
194261
|
-
*
|
|
194262
|
-
* `
|
|
194263
|
-
*
|
|
194264
|
-
*
|
|
194265
|
-
*
|
|
194266
|
-
*
|
|
194267
|
-
*
|
|
194268
|
-
*
|
|
194269
|
-
*
|
|
194655
|
+
* Lifecycle — driven from the program edge (`src/program.ts`), which DECIDES
|
|
194656
|
+
* with this module's pure/read-only halves and assembles the actual writes
|
|
194657
|
+
* into the two scripts a `gtd` command hands the external driver:
|
|
194658
|
+
*
|
|
194659
|
+
* - The CLOSE sequence (`decideCloseWindow` + `buildCloseWindowScript`) is
|
|
194660
|
+
* read-only in this module — it decides whether a window is open and safe
|
|
194661
|
+
* to close, keyed solely on `REVIEW_HEAD_REF` existing (the same
|
|
194662
|
+
* reviewed-branch/legacy-containment guards described below) — and
|
|
194663
|
+
* `program.ts`'s `buildRequiredScript` splices the resulting
|
|
194664
|
+
* `mixedResetTo`/`deleteRef`/`deleteRef` bash in as the LEAD steps of the
|
|
194665
|
+
* `required` script, ahead of the commit/squash steps themselves. Nothing
|
|
194666
|
+
* reads or mutates state until the driver actually runs that script; once
|
|
194667
|
+
* it does, HEAD/index are restored exactly, leaving only the reviewer's own
|
|
194668
|
+
* edits dirty (captured by the resting state's own `on` patterns like any
|
|
194669
|
+
* other pending change) — the pure machine never sees the window.
|
|
194670
|
+
* - The OPEN sequence (`decideOpenWindow` + `buildOpenWindowScript`) is
|
|
194671
|
+
* likewise read-only — it self-guards on the STEP'S OWN target state
|
|
194672
|
+
* declaring `reviewWindow: true` — and `program.ts`'s `openWindowScript`
|
|
194673
|
+
* turns that into the `optional` script's `updateRef`/`updateRef`/
|
|
194674
|
+
* `mixedResetTo`/`restoreStagedFrom` bash: saving HEAD to `REVIEW_HEAD_REF`
|
|
194675
|
+
* (the base to `REVIEW_BASE_REF`), then rewinding to the base. The driver
|
|
194676
|
+
* runs it AFTER the required script has already landed the new commit
|
|
194677
|
+
* (`buildOpenWindowScript`'s own doc comment explains why it pins the
|
|
194678
|
+
* literal string `"HEAD"`, never a resolved hash). It re-arms after
|
|
194679
|
+
* read-only commands (`gtd next` / `gtd status`) and refused invocations
|
|
194680
|
+
* too, so the editor's diff view stays consistent no matter which command
|
|
194681
|
+
* the loop last ran.
|
|
194682
|
+
*
|
|
194683
|
+
* Every close/open sequence is idempotent under re-entry, so a crash at any
|
|
194684
|
+
* point is recovered by the next invocation's close (the saved ref also keeps
|
|
194685
|
+
* the real head GC-reachable for the window's whole lifetime).
|
|
194270
194686
|
*/
|
|
194271
194687
|
/**
|
|
194272
194688
|
* The window's two state refs live in git's PER-WORKTREE `refs/worktree/*`
|
|
@@ -194284,7 +194700,7 @@ const planEntry = (rest, actor, entry) => gen(function* () {
|
|
|
194284
194700
|
const REVIEW_HEAD_REF = "refs/worktree/gtd/review-head";
|
|
194285
194701
|
const REVIEW_BASE_REF = "refs/worktree/gtd/review-base";
|
|
194286
194702
|
/**
|
|
194287
|
-
* The pre-7.2 SHARED refs. gtd never writes them any more; `
|
|
194703
|
+
* The pre-7.2 SHARED refs. gtd never writes them any more; `decideCloseWindow`
|
|
194288
194704
|
* only reads them to finish a window an older gtd left open across the upgrade
|
|
194289
194705
|
* — and only when HEAD is still contained in the saved head, since a shared ref
|
|
194290
194706
|
* may belong to a sibling worktree (see `openWindowRefs`).
|
|
@@ -194316,28 +194732,10 @@ const openWindowRefs = (git) => gen(function* () {
|
|
|
194316
194732
|
legacy: true
|
|
194317
194733
|
});
|
|
194318
194734
|
});
|
|
194319
|
-
|
|
194320
|
-
* Restore the real head if a review checkout window is open; no-op otherwise.
|
|
194321
|
-
* Keyed on the saved-head ref's existence (see `openWindowRefs`), NOT on any
|
|
194322
|
-
* config or machine state, so it always runs first and the machine never sees
|
|
194323
|
-
* the window.
|
|
194324
|
-
*
|
|
194325
|
-
* Fails loudly — refs left in place — when HEAD is no longer on the reviewed
|
|
194326
|
-
* branch (the base is not an ancestor of HEAD, e.g. after a branch switch): a
|
|
194327
|
-
* mixed reset there would rewrite the wrong branch's tip. Manual commits on
|
|
194328
|
-
* top of the base pass the guard: the reset keeps their content in the working
|
|
194329
|
-
* tree, where the next turn's capture picks it up as review feedback.
|
|
194330
|
-
*
|
|
194331
|
-
* A window found under the LEGACY shared refs gets one extra guard: a shared
|
|
194332
|
-
* ref may have been written by a sibling worktree, so the close only proceeds
|
|
194333
|
-
* when HEAD is contained in the saved head — the shape a window this worktree
|
|
194334
|
-
* opened itself has. Anything else refuses with the manual recovery rather
|
|
194335
|
-
* than resetting this branch onto another worktree's work (issue #118).
|
|
194336
|
-
*/
|
|
194337
|
-
const closeReviewWindow = gen(function* () {
|
|
194735
|
+
const decideCloseWindow = gen(function* () {
|
|
194338
194736
|
const git = yield* GitService;
|
|
194339
194737
|
const open = yield* openWindowRefs(git);
|
|
194340
|
-
if (isNone(open)) return {
|
|
194738
|
+
if (isNone(open)) return { shouldClose: false };
|
|
194341
194739
|
const refs = open.value;
|
|
194342
194740
|
const base = yield* git.readRefOption(refs.baseRef);
|
|
194343
194741
|
if (isSome(base) && base.value !== EMPTY_TREE$1) {
|
|
@@ -194346,44 +194744,65 @@ const closeReviewWindow = gen(function* () {
|
|
|
194346
194744
|
if (refs.legacy) {
|
|
194347
194745
|
if (!(yield* git.isAncestor("HEAD", refs.headHash))) return yield* fail$2(/* @__PURE__ */ new Error(`a review checkout window is recorded under the shared ${LEGACY_REVIEW_HEAD_REF} (written by gtd 7.1 or older) but it does not belong to this worktree — HEAD is not contained in it. gtd now scopes the window per worktree (${REVIEW_HEAD_REF}); in the worktree that owns it, run ${manualRecovery(refs)}`));
|
|
194348
194746
|
}
|
|
194349
|
-
|
|
194350
|
-
|
|
194351
|
-
|
|
194352
|
-
|
|
194747
|
+
return {
|
|
194748
|
+
shouldClose: true,
|
|
194749
|
+
refs
|
|
194750
|
+
};
|
|
194353
194751
|
});
|
|
194354
194752
|
/**
|
|
194355
|
-
*
|
|
194356
|
-
*
|
|
194357
|
-
*
|
|
194358
|
-
*
|
|
194359
|
-
*
|
|
194360
|
-
*
|
|
194361
|
-
*
|
|
194362
|
-
*
|
|
194363
|
-
*
|
|
194364
|
-
*
|
|
194365
|
-
*
|
|
194366
|
-
* is
|
|
194367
|
-
*
|
|
194368
|
-
*
|
|
194369
|
-
*
|
|
194370
|
-
*
|
|
194371
|
-
*
|
|
194372
|
-
*/
|
|
194373
|
-
const
|
|
194374
|
-
|
|
194375
|
-
|
|
194376
|
-
|
|
194377
|
-
|
|
194378
|
-
|
|
194379
|
-
|
|
194380
|
-
|
|
194381
|
-
|
|
194382
|
-
|
|
194383
|
-
|
|
194384
|
-
|
|
194385
|
-
|
|
194386
|
-
}
|
|
194753
|
+
* PURE: the close sequence's bash text — reset to the saved head, then delete
|
|
194754
|
+
* both refs — built from `src/GitScript.ts`'s `mixedResetTo`/`deleteRef`.
|
|
194755
|
+
* Assumes its caller's guards (the reviewed-branch check, the legacy
|
|
194756
|
+
* containment check — see `decideCloseWindow`) have already passed; this
|
|
194757
|
+
* builder performs none of them, only the write sequence for the case where
|
|
194758
|
+
* they did.
|
|
194759
|
+
*
|
|
194760
|
+
* The reset target is `refs.headHash` — the RESOLVED hash, not `refs.headRef`
|
|
194761
|
+
* by name — precisely so the sequence stays idempotent once the ref itself is
|
|
194762
|
+
* gone: the very next statement deletes `headRef`, so a script naming the ref
|
|
194763
|
+
* for the reset would fail on re-entry (the ref `git reset --mixed` would
|
|
194764
|
+
* need is the one the script just deleted). A hash stays resolvable
|
|
194765
|
+
* regardless, so a second run's reset is a true no-op (HEAD is already
|
|
194766
|
+
* there) and both deletes tolerate an already-missing ref. `legacy` on `refs`
|
|
194767
|
+
* goes unused here — the script only needs `headRef`/`baseRef`/`headHash` —
|
|
194768
|
+
* but the parameter carries the whole `WindowRefs` record so a caller can
|
|
194769
|
+
* pass `decideCloseWindow`'s resolved `refs` straight through.
|
|
194770
|
+
*/
|
|
194771
|
+
const buildCloseWindowScript = (refs) => [
|
|
194772
|
+
mixedResetTo(refs.headHash),
|
|
194773
|
+
deleteRef(refs.headRef),
|
|
194774
|
+
deleteRef(refs.baseRef)
|
|
194775
|
+
].join(" &&\n");
|
|
194776
|
+
const decideOpenWindow = (def, target, run, head) => {
|
|
194777
|
+
if (!isReviewWindowState(def, target)) return { shouldOpen: false };
|
|
194778
|
+
const base = reviewBaseFor(def, run);
|
|
194779
|
+
if (base === EMPTY_TREE$1 || base === head) return { shouldOpen: false };
|
|
194780
|
+
return {
|
|
194781
|
+
shouldOpen: true,
|
|
194782
|
+
base,
|
|
194783
|
+
head
|
|
194784
|
+
};
|
|
194785
|
+
};
|
|
194786
|
+
/**
|
|
194787
|
+
* PURE: the open sequence's bash text — base-ref write, head-ref write,
|
|
194788
|
+
* mixed-reset-to-base, then the `.gtd/` restore-staged-from pin, built from
|
|
194789
|
+
* `src/GitScript.ts`'s `updateRef`/`mixedResetTo`/`restoreStagedFrom`, in the
|
|
194790
|
+
* same crash-safe order `openReviewWindow` uses (base ref → head ref → mixed
|
|
194791
|
+
* reset → `.gtd/` pin). Deliberately no `git add --intent-to-add` anywhere —
|
|
194792
|
+
* new files stay untracked, for the exact two reasons `openReviewWindow`'s
|
|
194793
|
+
* own doc comment records — and no whole-tree index write: `restoreStagedFrom`
|
|
194794
|
+
* is scoped to `.gtd/` alone.
|
|
194795
|
+
*
|
|
194796
|
+
* Idempotent under re-entry: `updateRef`/`mixedResetTo` re-pointing at the
|
|
194797
|
+
* same target is a no-op, and `restoreStagedFrom` re-pinning an already-pinned
|
|
194798
|
+
* path is a no-op.
|
|
194799
|
+
*/
|
|
194800
|
+
const buildOpenWindowScript = (decision) => [
|
|
194801
|
+
updateRef(REVIEW_BASE_REF, decision.base),
|
|
194802
|
+
updateRef(REVIEW_HEAD_REF, decision.head),
|
|
194803
|
+
mixedResetTo(decision.base),
|
|
194804
|
+
restoreStagedFrom(REVIEW_HEAD_REF, [".gtd"])
|
|
194805
|
+
].join(" &&\n");
|
|
194387
194806
|
//#endregion
|
|
194388
194807
|
//#region node_modules/vscode-languageserver/lib/common/utils/is.js
|
|
194389
194808
|
var require_is$2 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
@@ -205438,17 +205857,17 @@ const resolveBuiltInMode = (mode) => {
|
|
|
205438
205857
|
};
|
|
205439
205858
|
const steeringCapabilities = (resolved) => {
|
|
205440
205859
|
if (resolved === void 0) return {};
|
|
205860
|
+
const seeded = resolved.builtIn !== void 0 && resolved.validate?.kind === "command" && isSeededValidateCommand(resolved.mode, resolved.validate.command);
|
|
205861
|
+
const liveValidate = resolved.builtIn !== void 0 && (resolved.validate?.kind === "builtin" || seeded) ? resolved.builtIn.validate : void 0;
|
|
205441
205862
|
return {
|
|
205442
205863
|
...resolved.builtIn !== void 0 ? { format: resolved.builtIn } : {},
|
|
205443
|
-
...
|
|
205444
|
-
...resolved.validate?.kind === "command" ? { externalValidate: true } : {}
|
|
205864
|
+
...liveValidate !== void 0 ? { liveValidate } : {},
|
|
205865
|
+
...resolved.validate?.kind === "command" && !seeded ? { externalValidate: true } : {}
|
|
205445
205866
|
};
|
|
205446
205867
|
};
|
|
205447
205868
|
/** The error message for a `mode:` that resolves to nothing — lists what the active workflow does know. */
|
|
205448
205869
|
const unknownModeMessage = (def, state, mode) => `state "${state}": mode "${mode}" is not defined by the active workflow (known modes: ${knownModes(def).join(", ")})`;
|
|
205449
205870
|
const errorText$1 = (e) => e instanceof Error ? e.message : String(e);
|
|
205450
|
-
/** How a non-zero exit reads in a message: a status number, or a signal death. */
|
|
205451
|
-
const exitText = (status) => status === null ? "a signal" : `status ${status}`;
|
|
205452
205871
|
/** Render one command template, turning an Eta failure into a named error rather than executing anything. */
|
|
205453
205872
|
const renderCommand = (mode, key, command, file, context) => try_({
|
|
205454
205873
|
try: () => renderModeCommand(command, {
|
|
@@ -205457,41 +205876,22 @@ const renderCommand = (mode, key, command, file, context) => try_({
|
|
|
205457
205876
|
}),
|
|
205458
205877
|
catch: (e) => /* @__PURE__ */ new Error(`mode "${mode}": "${key}" command failed to render — ${errorText$1(e)}`)
|
|
205459
205878
|
});
|
|
205460
|
-
/** The findings a non-zero `validate` exit reports: its output lines, or a synthesized line when it said nothing. */
|
|
205461
|
-
const findingsFrom = (mode, outcome) => {
|
|
205462
|
-
const lines = outcome.output.split("\n").map((line) => line.trimEnd()).filter((line) => line.length > 0);
|
|
205463
|
-
if (lines.length > 0) return lines;
|
|
205464
|
-
return [`mode "${mode}": validate command exited with ${exitText(outcome.status)} and no output`];
|
|
205465
|
-
};
|
|
205466
|
-
/**
|
|
205467
|
-
* Format `file` in place by running the mode's `format:` command. A mode with
|
|
205468
|
-
* no formatter (every mode, until a `modes:` layer declares one — gtd ships
|
|
205469
|
-
* none) formats nothing. A failing command is a hard error: the file is left
|
|
205470
|
-
* exactly as it was and the caller refuses, rather than validating a
|
|
205471
|
-
* half-formatted file.
|
|
205472
|
-
*/
|
|
205473
|
-
const formatSteeringFile = (resolved, file, context) => gen(function* () {
|
|
205474
|
-
const command = resolved.formatCommand;
|
|
205475
|
-
if (command === void 0) return;
|
|
205476
|
-
const rendered = yield* renderCommand(resolved.mode, "format", command, file, context);
|
|
205477
|
-
const outcome = yield* (yield* CommandRunner).bash(rendered);
|
|
205478
|
-
if (outcome.status !== 0) return yield* fail$2(/* @__PURE__ */ new Error(`mode "${resolved.mode}": format command exited with ${exitText(outcome.status)}${outcome.output.trim().length > 0 ? `:\n${outcome.output.trimEnd()}` : ""}`));
|
|
205479
|
-
});
|
|
205480
205879
|
/**
|
|
205481
|
-
*
|
|
205482
|
-
*
|
|
205483
|
-
*
|
|
205484
|
-
*
|
|
205485
|
-
*
|
|
205486
|
-
*
|
|
205487
|
-
|
|
205488
|
-
|
|
205489
|
-
|
|
205490
|
-
|
|
205491
|
-
|
|
205492
|
-
const
|
|
205493
|
-
|
|
205494
|
-
|
|
205880
|
+
* Render a mode's `format:`/`validate:` commands as plain strings, WITHOUT
|
|
205881
|
+
* running either — for a caller (an emitter that prints scripts rather than
|
|
205882
|
+
* executing them) that wants the commands themselves, not their outcome.
|
|
205883
|
+
* Unlike its siblings above, this needs no `CommandRunner`: it shares their
|
|
205884
|
+
* `renderCommand` helper for the rendering (and its errors) but never reaches
|
|
205885
|
+
* for a runner to execute the result. Order matches `formatAndValidateSteeringFile`
|
|
205886
|
+
* (format then validate); a half the mode doesn't declare — no `formatCommand`,
|
|
205887
|
+
* or a `validate` that is absent or `"builtin"` (an in-process parser has no
|
|
205888
|
+
* shell command to render) — is OMITTED from the result, not rendered as `""`.
|
|
205889
|
+
*/
|
|
205890
|
+
const renderSteeringCommands = (resolved, file, context) => gen(function* () {
|
|
205891
|
+
const commands = [];
|
|
205892
|
+
if (resolved.formatCommand !== void 0) commands.push(yield* renderCommand(resolved.mode, "format", resolved.formatCommand, file, context));
|
|
205893
|
+
if (resolved.validate?.kind === "command") commands.push(yield* renderCommand(resolved.mode, "validate", resolved.validate.command, file, context));
|
|
205894
|
+
return commands;
|
|
205495
205895
|
});
|
|
205496
205896
|
//#endregion
|
|
205497
205897
|
//#region src/Lsp.ts
|
|
@@ -206174,27 +206574,6 @@ const openInBrowser = (url) => {
|
|
|
206174
206574
|
//#region src/StepGuards.ts
|
|
206175
206575
|
/** Normalize every markdown checkbox to a single placeholder so a pure `[ ]`→`[x]` tick is invisible to a text comparison; any surviving difference is a human note. */
|
|
206176
206576
|
const normalizeCheckboxes = (content) => content.replace(/\[[ xX]\]/g, "[_]");
|
|
206177
|
-
const steeringFileGuard = {
|
|
206178
|
-
name: "steering-file",
|
|
206179
|
-
appliesTo: (rest) => rest.stateDef.mode !== void 0,
|
|
206180
|
-
prepare: (ctx) => gen(function* () {
|
|
206181
|
-
const mode = ctx.rest.stateDef.mode;
|
|
206182
|
-
const resolved = resolveSteeringMode(ctx.rest.def, mode);
|
|
206183
|
-
if (resolved === void 0) return yield* fail$2(new Error(unknownModeMessage(ctx.rest.def, ctx.rest.state, mode)));
|
|
206184
|
-
if ((yield* RepoFiles).working(ctx.file) === void 0) return;
|
|
206185
|
-
yield* formatSteeringFile(resolved, ctx.file, ctx.template);
|
|
206186
|
-
}),
|
|
206187
|
-
check: (ctx) => gen(function* () {
|
|
206188
|
-
const mode = ctx.rest.stateDef.mode;
|
|
206189
|
-
const resolved = resolveSteeringMode(ctx.rest.def, mode);
|
|
206190
|
-
if (resolved === void 0) return void 0;
|
|
206191
|
-
const current = yield* ctx.worktree;
|
|
206192
|
-
if (current === void 0) return void 0;
|
|
206193
|
-
const errors = yield* validateSteeringFile(resolved, ctx.file, current, ctx.template);
|
|
206194
|
-
if (errors.length === 0) return void 0;
|
|
206195
|
-
return `${ctx.file} is not valid at "${ctx.rest.state}" — fix these before stepping:\n${errors.map((e) => ` - ${e}`).join("\n")}`;
|
|
206196
|
-
})
|
|
206197
|
-
};
|
|
206198
206577
|
/** The `mode:` name of gtd's built-in REVIEW.md checkbox validator — the only mode the sign-off guard understands. */
|
|
206199
206578
|
const REVIEW_MODE = "review";
|
|
206200
206579
|
const reviewSignoffGuard = {
|
|
@@ -206224,9 +206603,8 @@ const feedbackProgressGuard = {
|
|
|
206224
206603
|
};
|
|
206225
206604
|
/** The `mode:` name of gtd's built-in open-questions checkbox format — the only mode the answer-completeness guard acts on. */
|
|
206226
206605
|
const QA_MODE = "qa";
|
|
206227
|
-
/** The
|
|
206606
|
+
/** The three step-capture guards, in EVALUATION order — message precedence when two would fire is observable, so this order is the contract (matches the old hand-copied call order, minus the now-removed steering-file guard). */
|
|
206228
206607
|
const stepGuards = [
|
|
206229
|
-
steeringFileGuard,
|
|
206230
206608
|
reviewSignoffGuard,
|
|
206231
206609
|
feedbackProgressGuard,
|
|
206232
206610
|
{
|
|
@@ -206242,7 +206620,7 @@ const stepGuards = [
|
|
|
206242
206620
|
];
|
|
206243
206621
|
/**
|
|
206244
206622
|
* The `gtd step` capture gate: run every guard the resolved rest's state
|
|
206245
|
-
* applies to
|
|
206623
|
+
* applies to against ONE sample of the current committed/working bytes — see
|
|
206246
206624
|
* the module docstring — and fail with the first refusal's reason, prefixed
|
|
206247
206625
|
* `gtd step <invoker>: ` (the one place that prefix, and the `cliErrorLine`
|
|
206248
206626
|
* `/^gtd[: ]/` contract, is satisfied). A no-op for a squash/no-op decision, a
|
|
@@ -206264,7 +206642,6 @@ const enforceStepGuards = (input) => gen(function* () {
|
|
|
206264
206642
|
hasCodeChange,
|
|
206265
206643
|
template: input.context
|
|
206266
206644
|
};
|
|
206267
|
-
for (const g of applicable) if (g.prepare !== void 0) yield* g.prepare(base);
|
|
206268
206645
|
const files = yield* RepoFiles;
|
|
206269
206646
|
const head = yield* cached(files.committed(file));
|
|
206270
206647
|
const worktree = yield* cached(try_(() => files.working(file)));
|
|
@@ -206278,37 +206655,6 @@ const enforceStepGuards = (input) => gen(function* () {
|
|
|
206278
206655
|
if (refusal !== void 0) return yield* fail$2(/* @__PURE__ */ new Error(`gtd step ${input.invoker}: ${refusal}`));
|
|
206279
206656
|
}
|
|
206280
206657
|
});
|
|
206281
|
-
/**
|
|
206282
|
-
* `gtd validate`'s shared evaluation: format the resolved rest's steering
|
|
206283
|
-
* file in place, then validate its formatted contents — the SAME
|
|
206284
|
-
* format-then-validate pair the `steering-file` guard's `prepare`/`check`
|
|
206285
|
-
* split runs, so both format and check the same way (an agent's fresh draft
|
|
206286
|
-
* and a human's edit are treated alike). `present` is false — and nothing is
|
|
206287
|
-
* formatted or validated — when the state declares no steering file, or the
|
|
206288
|
-
* file is absent (e.g. a human deleted `.gtd/REVIEW.md` to approve).
|
|
206289
|
-
*/
|
|
206290
|
-
const checkSteeringFile = (rest, context, file) => gen(function* () {
|
|
206291
|
-
const mode = rest.stateDef.mode;
|
|
206292
|
-
if (file === void 0 || mode === void 0) return {
|
|
206293
|
-
file,
|
|
206294
|
-
present: false,
|
|
206295
|
-
errors: []
|
|
206296
|
-
};
|
|
206297
|
-
const files = yield* RepoFiles;
|
|
206298
|
-
if (files.working(file) === void 0) return {
|
|
206299
|
-
file,
|
|
206300
|
-
present: false,
|
|
206301
|
-
errors: []
|
|
206302
|
-
};
|
|
206303
|
-
const resolved = resolveSteeringMode(rest.def, mode);
|
|
206304
|
-
if (resolved === void 0) return yield* fail$2(new Error(unknownModeMessage(rest.def, rest.state, mode)));
|
|
206305
|
-
yield* formatSteeringFile(resolved, file, context);
|
|
206306
|
-
return {
|
|
206307
|
-
file,
|
|
206308
|
-
present: true,
|
|
206309
|
-
errors: yield* validateSteeringFile(resolved, file, files.working(file) ?? "", context)
|
|
206310
|
-
};
|
|
206311
|
-
});
|
|
206312
206658
|
//#endregion
|
|
206313
206659
|
//#region src/program.ts
|
|
206314
206660
|
/**
|
|
@@ -206353,57 +206699,144 @@ const runInitCommand = (json, write) => gen(function* () {
|
|
|
206353
206699
|
}) + "\n");
|
|
206354
206700
|
else write("Wrote .gtdrc.json seeding the default variables (the test command) and a\nPrettier formatting suggestion. gtd runs its built-in workflow by default — add\na workflow: key only if you want to customize the machine itself.\n\n" + (inRepo ? "Review and commit it before starting: an uncommitted .gtdrc.json counts as a\npending change, so the initial state would capture it on the first step. Once\ncommitted, run `gtd step human` to begin.\n" : "This directory is not a git repository, so there is nothing to commit here. The\nconfig applies to any gtd repository nested below it — gtd discovers it by\nwalking up from the repository root. Run `gtd step human` from such a repo to\nbegin.\n"));
|
|
206355
206701
|
});
|
|
206702
|
+
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
206703
|
+
/** The `EmitPreconditions` every script this file assembles asserts against: the resolved HEAD hash the snapshot driving it was taken at — `""` when the repo has no commits yet, `headAssertion`'s own convention for that case. */
|
|
206704
|
+
const headPreconditions = (currentCommit) => ({ expectedHead: currentCommit });
|
|
206705
|
+
/**
|
|
206706
|
+
* The resting state's own steering-mode commands, embedded ahead of the
|
|
206707
|
+
* commit — the step script now carries its own format/validate pair instead
|
|
206708
|
+
* of gtd running it in process (see `src/StepGuards.ts`'s shrunk registry).
|
|
206709
|
+
* Empty for a state declaring no `file:`+`mode:` pair; an unknown mode name
|
|
206710
|
+
* is a refusal, exactly as it was when gtd ran the commands itself.
|
|
206711
|
+
*/
|
|
206712
|
+
const steeringModeSteps = (rest) => gen(function* () {
|
|
206713
|
+
const file = rest.hints.file;
|
|
206714
|
+
const mode = rest.stateDef.mode;
|
|
206715
|
+
if (file === void 0 || mode === void 0) return [];
|
|
206716
|
+
const resolved = resolveSteeringMode(rest.def, mode);
|
|
206717
|
+
if (resolved === void 0) return yield* fail$2(new Error(unknownModeMessage(rest.def, rest.state, mode)));
|
|
206718
|
+
return (yield* renderSteeringCommands(resolved, file, rest.context)).map((command) => ({
|
|
206719
|
+
kind: "command",
|
|
206720
|
+
command
|
|
206721
|
+
}));
|
|
206722
|
+
});
|
|
206723
|
+
/**
|
|
206724
|
+
* The `optional` half: re-open the review checkout window when the step lands
|
|
206725
|
+
* at a `reviewWindow: true` state, `""` otherwise.
|
|
206726
|
+
*
|
|
206727
|
+
* `"HEAD"` is deliberately the literal string, not a resolved hash — git
|
|
206728
|
+
* resolves it at the moment this script runs, which is after the required
|
|
206729
|
+
* script has already landed the new commit (see `ReviewWindow.ts`'s
|
|
206730
|
+
* `decideOpenWindow` doc comment). For the same reason it carries no
|
|
206731
|
+
* `expectedHead` precondition: the HEAD it will see is a hash that does not
|
|
206732
|
+
* exist yet at decide time (see `EmitPreconditions`).
|
|
206733
|
+
*/
|
|
206734
|
+
const openWindowScript = (rest, targetState) => {
|
|
206735
|
+
const decision = decideOpenWindow(rest.def, targetState, rest.run, "HEAD");
|
|
206736
|
+
if (!decision.shouldOpen) return "";
|
|
206737
|
+
return emitScripts({}, [], [{
|
|
206738
|
+
kind: "gitWrite",
|
|
206739
|
+
command: buildOpenWindowScript({
|
|
206740
|
+
base: decision.base,
|
|
206741
|
+
head: "HEAD"
|
|
206742
|
+
})
|
|
206743
|
+
}]).optional;
|
|
206744
|
+
};
|
|
206745
|
+
/**
|
|
206746
|
+
* The subject the required script WILL produce. A `"commit"` decision's is
|
|
206747
|
+
* fully deterministic already; a `"squash"` decision's is only known once its
|
|
206748
|
+
* template is rendered. Unreachable-on-failure: `renderDecision` already
|
|
206749
|
+
* rendered the same template and refused the command if it threw, so this
|
|
206750
|
+
* second render can never be the first to fail.
|
|
206751
|
+
*/
|
|
206752
|
+
const previewSubject = (decision, rest) => decision.kind === "commit" ? succeed$3(decision.subject) : try_(() => renderStateTemplate(decision.template, rest.context).split("\n")[0].trim()).pipe(catchAll(() => succeed$3(null)));
|
|
206753
|
+
/**
|
|
206754
|
+
* The `required` half: everything that decides what lands in git, in order —
|
|
206755
|
+
* the review-window CLOSE (when one is open), the resting state's own
|
|
206756
|
+
* steering-mode format/validate commands, then the commit/squash steps
|
|
206757
|
+
* themselves.
|
|
206758
|
+
*/
|
|
206759
|
+
const buildRequiredScript = (rest, decision, invoker, cost, model) => gen(function* () {
|
|
206760
|
+
const git = yield* GitService;
|
|
206761
|
+
const closeDecision = yield* decideCloseWindow;
|
|
206762
|
+
const commitContext = decision.kind === "commit" ? rest.context : yield* contextAt(rest, decision.state, invoker, cost, model);
|
|
206763
|
+
const steps = [
|
|
206764
|
+
...closeDecision.shouldClose ? [{
|
|
206765
|
+
kind: "gitWrite",
|
|
206766
|
+
command: buildCloseWindowScript(closeDecision.refs)
|
|
206767
|
+
}] : [],
|
|
206768
|
+
...yield* steeringModeSteps(rest),
|
|
206769
|
+
...yield* renderDecision(git, rest, decision, commitContext, cost, model)
|
|
206770
|
+
];
|
|
206771
|
+
return emitScripts(headPreconditions(rest.context.currentCommit), steps).required;
|
|
206772
|
+
});
|
|
206773
|
+
/** A step's `--cost`/`--model` as `planStep` options — each key OMITTED (never `undefined`-valued) when the flag was not given. */
|
|
206774
|
+
const stepOptions = (cost, model) => ({
|
|
206775
|
+
...cost !== void 0 ? { cost } : {},
|
|
206776
|
+
...model !== void 0 ? { model } : {}
|
|
206777
|
+
});
|
|
206356
206778
|
/**
|
|
206357
|
-
* Authenticate `invoker` against the currently resolved rest and
|
|
206358
|
-
* one resulting transition (commit or squash) for `gtd step
|
|
206359
|
-
* `planStep` decides; the
|
|
206360
|
-
* `
|
|
206361
|
-
*
|
|
206362
|
-
*
|
|
206363
|
-
*
|
|
206779
|
+
* Authenticate `invoker` against the currently resolved rest and decide the
|
|
206780
|
+
* one resulting transition (commit or squash) for `gtd step` — WITHOUT
|
|
206781
|
+
* performing it. `currentRest` → `planStep` decides; the step-capture guards
|
|
206782
|
+
* (`src/StepGuards.ts`) then refuse before anything is emitted, exactly as
|
|
206783
|
+
* before. What used to be `plan.perform` is now assembled by hand into a
|
|
206784
|
+
* `required` script (the review-window CLOSE, when one is open; the resting
|
|
206785
|
+
* state's steering-mode format/validate commands, when it declares
|
|
206786
|
+
* `file:`+`mode:`; then the commit/squash steps themselves, via
|
|
206787
|
+
* `renderDecision`) and an `optional` one (the review-window OPEN, when the
|
|
206788
|
+
* target declares `reviewWindow: true`) — see the "Review-window management"
|
|
206789
|
+
* recipe this mirrors. Refusals fail the Effect with a formatted message; a
|
|
206790
|
+
* no-op returns `subject: null` and empty scripts (exit zero, nothing to
|
|
206791
|
+
* run).
|
|
206792
|
+
*
|
|
206793
|
+
* `plan.scripts` (built by `Edge.ts` itself) is NOT reused here: its baked-in
|
|
206794
|
+
* precondition uses `rest.context.currentCommit`, the LITERAL git HEAD at
|
|
206795
|
+
* decide time — which, while a review window is open, is the window's BASE,
|
|
206796
|
+
* not the real head — and it contains no window close/open steps at all. This
|
|
206797
|
+
* function builds its own required/optional pair to sidestep that trap.
|
|
206364
206798
|
*/
|
|
206365
206799
|
const stepAsActor = (invoker, cost, model) => gen(function* () {
|
|
206366
206800
|
const rest = yield* currentRest;
|
|
206367
|
-
const plan = yield* planStep(rest, invoker,
|
|
206368
|
-
...cost !== void 0 ? { cost } : {},
|
|
206369
|
-
...model !== void 0 ? { model } : {}
|
|
206370
|
-
});
|
|
206801
|
+
const plan = yield* planStep(rest, invoker, stepOptions(cost, model));
|
|
206371
206802
|
if (plan.kind === "refusal") return yield* fail$2(new Error(plan.message));
|
|
206372
206803
|
if (plan.kind === "noop") return {
|
|
206373
206804
|
state: plan.state,
|
|
206374
206805
|
subject: null,
|
|
206375
206806
|
cost: null,
|
|
206376
|
-
model: null
|
|
206807
|
+
model: null,
|
|
206808
|
+
required: "",
|
|
206809
|
+
optional: ""
|
|
206377
206810
|
};
|
|
206811
|
+
const decision = plan.decision;
|
|
206812
|
+
if (decision.kind !== "commit" && decision.kind !== "squash") return yield* fail$2(/* @__PURE__ */ new Error(`gtd: internal error — plan kind "${plan.kind}" but decision kind "${decision.kind}"`));
|
|
206378
206813
|
yield* enforceStepGuards({
|
|
206379
206814
|
rest,
|
|
206380
206815
|
context: rest.context,
|
|
206381
206816
|
file: rest.hints.file,
|
|
206382
206817
|
changes: rest.changes,
|
|
206383
206818
|
invoker,
|
|
206384
|
-
kind:
|
|
206819
|
+
kind: decision.kind
|
|
206385
206820
|
});
|
|
206386
|
-
const
|
|
206387
|
-
if (outcome.kind === "reset" || outcome.kind === "noop") return {
|
|
206388
|
-
state: outcome.state,
|
|
206389
|
-
subject: null,
|
|
206390
|
-
cost: null,
|
|
206391
|
-
model: null
|
|
206392
|
-
};
|
|
206821
|
+
const targetState = decision.kind === "commit" ? decision.to : decision.state;
|
|
206393
206822
|
return {
|
|
206394
206823
|
state: rest.state,
|
|
206395
|
-
subject:
|
|
206824
|
+
subject: yield* previewSubject(decision, rest),
|
|
206396
206825
|
cost: cost ?? null,
|
|
206397
|
-
model: model ?? null
|
|
206826
|
+
model: model ?? null,
|
|
206827
|
+
required: yield* buildRequiredScript(rest, decision, invoker, cost, model),
|
|
206828
|
+
optional: openWindowScript(rest, targetState)
|
|
206398
206829
|
};
|
|
206399
206830
|
});
|
|
206400
|
-
/** Renders `stepAsActor`'s result for `gtd step`. */
|
|
206831
|
+
/** Renders `stepAsActor`'s result for `gtd step`. Plain text names the (previewed) resulting subject exactly as before; `--json` additionally carries `required`/`optional` — the bash the external driver runs to actually land it. */
|
|
206401
206832
|
const reportStepResult = (result, json, write) => {
|
|
206402
206833
|
if (json) write(JSON.stringify({
|
|
206403
206834
|
state: result.state,
|
|
206404
206835
|
subject: result.subject,
|
|
206405
206836
|
...result.cost !== null ? { cost: result.cost } : {},
|
|
206406
|
-
...result.model !== null ? { model: result.model } : {}
|
|
206837
|
+
...result.model !== null ? { model: result.model } : {},
|
|
206838
|
+
required: result.required,
|
|
206839
|
+
optional: result.optional
|
|
206407
206840
|
}) + "\n");
|
|
206408
206841
|
else write(result.subject !== null ? `committed: ${result.subject}\n` : `nothing to do at "${result.state}"\n`);
|
|
206409
206842
|
};
|
|
@@ -206461,14 +206894,14 @@ const runEntryCommand = (actor, entryState, varOverrides, json, write, commandLa
|
|
|
206461
206894
|
vars: varOverrides
|
|
206462
206895
|
});
|
|
206463
206896
|
if (plan.kind === "refusal") return yield* fail$2(new Error(plan.message));
|
|
206464
|
-
yield* plan.perform;
|
|
206465
206897
|
if (json) write(JSON.stringify({
|
|
206466
206898
|
state: plan.state,
|
|
206467
|
-
subject: plan.subject
|
|
206899
|
+
subject: plan.subject,
|
|
206900
|
+
required: plan.scripts.required,
|
|
206901
|
+
optional: plan.scripts.optional
|
|
206468
206902
|
}) + "\n");
|
|
206469
206903
|
else write(`committed: ${plan.subject}\n`);
|
|
206470
206904
|
});
|
|
206471
|
-
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
206472
206905
|
/**
|
|
206473
206906
|
* `gtd abandon`: end the process currently underway WITHOUT completing it,
|
|
206474
206907
|
* returning the machine to the workflow's initial state — the recovery path out
|
|
@@ -206500,6 +206933,13 @@ const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
|
206500
206933
|
* that fails when there is nothing to recover is a worse tool. The one refusal
|
|
206501
206934
|
* is a process whose first commit is the repository's own root commit: there is
|
|
206502
206935
|
* no earlier commit to rewind to.
|
|
206936
|
+
*
|
|
206937
|
+
* The read-side refusal/no-op checks above stay direct `GitService` reads (the
|
|
206938
|
+
* documented exception, `AGENTS.md`) — abandon must still work when a `Rest`
|
|
206939
|
+
* would refuse. The actual mutation (retaining history, then the mixed reset)
|
|
206940
|
+
* is no longer PERFORMED here: it's emitted as a `required` bash script for
|
|
206941
|
+
* the external driver to run, built from `src/GitScript.ts`'s pure
|
|
206942
|
+
* `updateRef`/`mixedResetTo` — narrowing the exception further still.
|
|
206503
206943
|
*/
|
|
206504
206944
|
const runAbandonCommand = (json, write) => gen(function* () {
|
|
206505
206945
|
const git = yield* GitService;
|
|
@@ -206516,14 +206956,31 @@ const runAbandonCommand = (json, write) => gen(function* () {
|
|
|
206516
206956
|
}
|
|
206517
206957
|
const restState = run.trace[run.trace.length - 1].state;
|
|
206518
206958
|
if (run.startParentHash === EMPTY_TREE) return yield* fail$2(/* @__PURE__ */ new Error(`gtd abandon: the process underway (resting at "${restState}") starts at the repository's first commit — there is no earlier commit to rewind to`));
|
|
206519
|
-
|
|
206520
|
-
yield* git.
|
|
206521
|
-
const subject = yield* git.lastCommitSubject();
|
|
206959
|
+
const closeDecision = yield* decideCloseWindow;
|
|
206960
|
+
const tip = closeDecision.shouldClose ? closeDecision.refs.headHash : yield* git.resolveRef("HEAD");
|
|
206961
|
+
const subject = yield* git.lastCommitSubject(run.startParentHash);
|
|
206962
|
+
const steps = [
|
|
206963
|
+
...closeDecision.shouldClose ? [{
|
|
206964
|
+
kind: "gitWrite",
|
|
206965
|
+
command: buildCloseWindowScript(closeDecision.refs)
|
|
206966
|
+
}] : [],
|
|
206967
|
+
{
|
|
206968
|
+
kind: "gitWrite",
|
|
206969
|
+
command: updateRef(HISTORY_REF, tip)
|
|
206970
|
+
},
|
|
206971
|
+
{
|
|
206972
|
+
kind: "gitWrite",
|
|
206973
|
+
command: mixedResetTo(run.startParentHash)
|
|
206974
|
+
}
|
|
206975
|
+
];
|
|
206976
|
+
const required = emitScripts(headPreconditions(yield* git.resolveRef("HEAD")), steps).required;
|
|
206522
206977
|
if (json) write(JSON.stringify({
|
|
206523
206978
|
state: initial,
|
|
206524
206979
|
abandoned: true,
|
|
206525
206980
|
from: restState,
|
|
206526
|
-
head: run.startParentHash
|
|
206981
|
+
head: run.startParentHash,
|
|
206982
|
+
required,
|
|
206983
|
+
optional: ""
|
|
206527
206984
|
}) + "\n");
|
|
206528
206985
|
else write(`abandoned the process resting at "${restState}" — HEAD is back at ${run.startParentHash.slice(0, 7)} ("${subject}"), resting at "${initial}".\nEverything the process produced is kept as uncommitted changes (\`git status\`); discard them with \`git checkout -- . && git clean -fd .gtd\` for a clean tree.\n`);
|
|
206529
206986
|
});
|
|
@@ -206537,7 +206994,10 @@ const runAbandonCommand = (json, write) => gen(function* () {
|
|
|
206537
206994
|
* Guarded by `restorability` so it never discards work it didn't create:
|
|
206538
206995
|
* refuses on a dirty working tree, when there is no retained history, and
|
|
206539
206996
|
* when HEAD has advanced past the retained tip with commits that would be
|
|
206540
|
-
* lost by resetting.
|
|
206997
|
+
* lost by resetting. Those checks stay direct `GitService` reads (same
|
|
206998
|
+
* documented exception as `gtd abandon`); the mutation itself (the hard
|
|
206999
|
+
* reset, then clearing the retained-history ref) is emitted as a `required`
|
|
207000
|
+
* script instead of performed here.
|
|
206541
207001
|
*/
|
|
206542
207002
|
const runRestoreCommand = (json, write) => gen(function* () {
|
|
206543
207003
|
const git = yield* GitService;
|
|
@@ -206549,31 +207009,69 @@ const runRestoreCommand = (json, write) => gen(function* () {
|
|
|
206549
207009
|
const headHash = yield* git.resolveRef("HEAD");
|
|
206550
207010
|
const check = yield* restorability(git, headHash, yield* git.lastCommitMessage(), tip);
|
|
206551
207011
|
if (!check.ok) return yield* fail$2(/* @__PURE__ */ new Error(`gtd restore: ${check.reason} — HEAD ${headHash.slice(0, 7)} is ahead of the retained tip ${tip.slice(0, 7)}.`));
|
|
206552
|
-
yield*
|
|
206553
|
-
yield*
|
|
206554
|
-
const
|
|
206555
|
-
|
|
207012
|
+
const after = yield* restAt(tip);
|
|
207013
|
+
const subject = yield* git.lastCommitSubject(tip);
|
|
207014
|
+
const steps = [{
|
|
207015
|
+
kind: "gitWrite",
|
|
207016
|
+
command: hardResetTo(tip)
|
|
207017
|
+
}, {
|
|
207018
|
+
kind: "gitWrite",
|
|
207019
|
+
command: deleteRef(HISTORY_REF)
|
|
207020
|
+
}];
|
|
207021
|
+
const required = emitScripts(headPreconditions(headHash), steps).required;
|
|
206556
207022
|
if (json) write(JSON.stringify({
|
|
206557
207023
|
state: after.state,
|
|
206558
207024
|
restored: true,
|
|
206559
207025
|
to: tip,
|
|
206560
|
-
from: before.state
|
|
207026
|
+
from: before.state,
|
|
207027
|
+
required,
|
|
207028
|
+
optional: ""
|
|
206561
207029
|
}) + "\n");
|
|
206562
207030
|
else write(`restored the retained history — HEAD is back at ${tip.slice(0, 7)} ("${subject}"), resting at "${after.state}". Resume with the loop, or \`git reset\` to any earlier turn to restart from there.\n`);
|
|
206563
207031
|
});
|
|
206564
207032
|
/**
|
|
207033
|
+
* The mode's own resolved VALIDATE command, rendered against `file` — the
|
|
207034
|
+
* command `selfValidateInstruction` names. Resolution mirrors
|
|
207035
|
+
* `renderSteeringCommands`'s own layering (`src/SteeringMode.ts`) but picks
|
|
207036
|
+
* out the validate half alone: a declared `validate:` command (or, for
|
|
207037
|
+
* `qa`/`review`, `PatternConfig.ts`'s own seeded `gtd check <mode> '<file>'`
|
|
207038
|
+
* default — every compiled definition carries ONE of the two) renders as the
|
|
207039
|
+
* LAST command `renderSteeringCommands` would emit (format, if any, always
|
|
207040
|
+
* comes first); a mode resolving to a `"builtin"` validator (no shell command
|
|
207041
|
+
* at all — only reachable when `resolveSteeringMode` is asked about a mode
|
|
207042
|
+
* outside the active definition's own compiled `modes:`, since a real
|
|
207043
|
+
* definition always seeds one) or to no mode at all names the leaf
|
|
207044
|
+
* `gtd check <mode> '<file>'` invocation directly instead
|
|
207045
|
+
* (`seededValidateCommand`), so there is always something concrete to name.
|
|
207046
|
+
*/
|
|
207047
|
+
const resolveSelfValidateCommand = (def, mode, file, context) => gen(function* () {
|
|
207048
|
+
const resolved = resolveSteeringMode(def, mode);
|
|
207049
|
+
if (resolved?.validate?.kind === "command") {
|
|
207050
|
+
const rendered = yield* renderSteeringCommands(resolved, file, context);
|
|
207051
|
+
const validateCommand = rendered[rendered.length - 1];
|
|
207052
|
+
if (validateCommand !== void 0) return validateCommand;
|
|
207053
|
+
}
|
|
207054
|
+
return yield* try_({
|
|
207055
|
+
try: () => renderModeCommand(seededValidateCommand(mode), {
|
|
207056
|
+
...context,
|
|
207057
|
+
file
|
|
207058
|
+
}),
|
|
207059
|
+
catch: (e) => e instanceof Error ? e : new Error(String(e))
|
|
207060
|
+
});
|
|
207061
|
+
});
|
|
207062
|
+
/**
|
|
206565
207063
|
* The self-validation instruction gtd APPENDS to a `prompt` rest that declares
|
|
206566
207064
|
* both `file:` and `mode:` — i.e. a state whose actor hands over a steering
|
|
206567
|
-
* file
|
|
206568
|
-
*
|
|
206569
|
-
*
|
|
206570
|
-
*
|
|
206571
|
-
*
|
|
206572
|
-
*
|
|
206573
|
-
*
|
|
206574
|
-
*
|
|
206575
|
-
*/
|
|
206576
|
-
const selfValidateInstruction = (file) => `\nBefore finishing your turn, run \`
|
|
207065
|
+
* file some command validates. Appended ONLY to plain `gtd next` output (for a
|
|
207066
|
+
* human or a simple driver who reads the prompt and hands it to an agent, so
|
|
207067
|
+
* the agent self-validates); withheld from `gtd next --json`, where the
|
|
207068
|
+
* driving loop instead runs `gtd validate` after the turn and re-prompts on
|
|
207069
|
+
* findings (see the `bin/gtd` loop driver). This is advisory: `gtd step`
|
|
207070
|
+
* embeds that same command ahead of its own commit and REFUSES a turn whose
|
|
207071
|
+
* steering file is invalid (see `stepAsActor`), so a malformed file is never
|
|
207072
|
+
* captured whether or not this instruction was followed.
|
|
207073
|
+
*/
|
|
207074
|
+
const selfValidateInstruction = (command, file) => `\nBefore finishing your turn, run \`${command}\` — it checks ${file} — and fix every violation it reports until it exits cleanly. Do not finish while it still reports violations.\n`;
|
|
206577
207075
|
/** True when a rendered rest is a `prompt` turn that hands over a validatable steering file (`file:`+`mode:` both declared). */
|
|
206578
207076
|
const emitsValidatablePrompt = (rendered) => rendered.kind === "prompt" && rendered.file !== void 0 && rendered.mode !== void 0;
|
|
206579
207077
|
/** `gtd next [--json]`: pure emitter of the resolved rest's rendered content (no mutation). */
|
|
@@ -206590,51 +207088,116 @@ const nextJsonOutput = (rendered) => JSON.stringify({
|
|
|
206590
207088
|
...rendered.mode !== void 0 ? { mode: rendered.mode } : {},
|
|
206591
207089
|
...rendered.edges.length > 0 ? { edges: rendered.edges } : {}
|
|
206592
207090
|
}) + "\n";
|
|
206593
|
-
/** `gtd next`'s plain-text output: the rendered content (newline-terminated), plus the self-validation instruction when the rest is a validatable prompt
|
|
206594
|
-
const nextPlainOutput = (rendered) => {
|
|
207091
|
+
/** `gtd next`'s plain-text output: the rendered content (newline-terminated), plus the self-validation instruction (naming `selfValidateCommand`, when resolved) when the rest is a validatable prompt. */
|
|
207092
|
+
const nextPlainOutput = (rendered, selfValidateCommand) => {
|
|
206595
207093
|
const base = rendered.content.endsWith("\n") ? rendered.content : rendered.content + "\n";
|
|
206596
|
-
return emitsValidatablePrompt(rendered) ? base + selfValidateInstruction(rendered.file) : base;
|
|
207094
|
+
return emitsValidatablePrompt(rendered) && selfValidateCommand !== void 0 ? base + selfValidateInstruction(selfValidateCommand, rendered.file) : base;
|
|
206597
207095
|
};
|
|
206598
207096
|
const runNextCommand = (json, write) => gen(function* () {
|
|
206599
|
-
const
|
|
206600
|
-
|
|
207097
|
+
const rest = yield* currentRest;
|
|
207098
|
+
const rendered = yield* renderRest(rest);
|
|
207099
|
+
const selfValidateCommand = !json && emitsValidatablePrompt(rendered) ? yield* resolveSelfValidateCommand(rest.def, rendered.mode, rendered.file, rest.context).pipe(catchAll(() => succeed$3(void 0))) : void 0;
|
|
207100
|
+
write(json ? nextJsonOutput(rendered) : nextPlainOutput(rendered, selfValidateCommand));
|
|
206601
207101
|
});
|
|
206602
207102
|
/**
|
|
206603
|
-
* `gtd validate [--json]`: format (in place) then
|
|
206604
|
-
* the resolved rest declares (`file:` rendered,
|
|
206605
|
-
*
|
|
206606
|
-
*
|
|
206607
|
-
*
|
|
206608
|
-
*
|
|
206609
|
-
*
|
|
206610
|
-
*
|
|
206611
|
-
*
|
|
206612
|
-
*
|
|
207103
|
+
* `gtd validate [--json]`: emit the script that would format (in place) then
|
|
207104
|
+
* validate the steering file the resolved rest declares (`file:` rendered,
|
|
207105
|
+
* `mode:` selecting how) — the SAME commands `gtd step`'s capture guard now
|
|
207106
|
+
* embeds ahead of its own commit (see `stepAsActor`), rendered here for a
|
|
207107
|
+
* human or agent to run directly. gtd itself reads no file and executes
|
|
207108
|
+
* nothing: a state with no `file:`/`mode:`, or a file absent from the working
|
|
207109
|
+
* tree, has nothing to validate (`script: ""`, exit 0 either way) — the
|
|
207110
|
+
* verdict now lives in the emitted script's own future exit code, not this
|
|
207111
|
+
* command's, so this never fails on a bad file. `RepoFiles`/`FileSystem` is
|
|
207112
|
+
* used only to check the file's PRESENCE, never to read or judge its content.
|
|
206613
207113
|
*/
|
|
206614
207114
|
const runValidateCommand = (json, write) => gen(function* () {
|
|
206615
207115
|
const rest = yield* currentRest;
|
|
206616
|
-
const
|
|
206617
|
-
|
|
207116
|
+
const file = rest.hints.file;
|
|
207117
|
+
const mode = rest.stateDef.mode;
|
|
207118
|
+
const emitNothingToValidate = () => {
|
|
206618
207119
|
if (json) write(JSON.stringify({
|
|
206619
207120
|
state: rest.state,
|
|
206620
|
-
|
|
206621
|
-
|
|
207121
|
+
...file !== void 0 ? { file } : {},
|
|
207122
|
+
...mode !== void 0 ? { mode } : {},
|
|
207123
|
+
script: ""
|
|
206622
207124
|
}) + "\n");
|
|
206623
207125
|
else write(`nothing to validate at "${rest.state}"\n`);
|
|
207126
|
+
};
|
|
207127
|
+
if (file === void 0 || mode === void 0) {
|
|
207128
|
+
emitNothingToValidate();
|
|
207129
|
+
return;
|
|
207130
|
+
}
|
|
207131
|
+
const fs = yield* FileSystem;
|
|
207132
|
+
const toError = (e) => e instanceof Error ? e : new Error(String(e));
|
|
207133
|
+
if (!(yield* fs.exists(file).pipe(mapError$1(toError)))) {
|
|
207134
|
+
emitNothingToValidate();
|
|
206624
207135
|
return;
|
|
206625
207136
|
}
|
|
207137
|
+
const resolved = resolveSteeringMode(rest.def, mode);
|
|
207138
|
+
if (resolved === void 0) return yield* fail$2(new Error(unknownModeMessage(rest.def, rest.state, mode)));
|
|
207139
|
+
const commands = yield* renderSteeringCommands(resolved, file, rest.context);
|
|
207140
|
+
const script = emitScripts(headPreconditions(rest.context.currentCommit), commands.map((command) => ({
|
|
207141
|
+
kind: "command",
|
|
207142
|
+
command
|
|
207143
|
+
}))).required;
|
|
207144
|
+
if (json) write(JSON.stringify({
|
|
207145
|
+
state: rest.state,
|
|
207146
|
+
file,
|
|
207147
|
+
mode,
|
|
207148
|
+
script
|
|
207149
|
+
}) + "\n");
|
|
207150
|
+
else write(script.length > 0 ? `${script}\n` : `nothing to validate at "${rest.state}"\n`);
|
|
207151
|
+
});
|
|
207152
|
+
/**
|
|
207153
|
+
* `gtd check <mode> <file>`: read `<file>` and run the BUILT-IN steering
|
|
207154
|
+
* format named `<mode>`'s pure parser over its contents, printing each
|
|
207155
|
+
* finding one per line and exiting non-zero when there are any. Unlike `gtd
|
|
207156
|
+
* validate` (which resolves the currently resting state's own `file:`/`mode:`
|
|
207157
|
+
* and formats before validating), this resolves NO workflow state and reads
|
|
207158
|
+
* NO config — both `mode` and `file` are given explicitly, so it needs only
|
|
207159
|
+
* `FileSystem.FileSystem` and runs from any directory. This is what a
|
|
207160
|
+
* workflow's seeded `validate:` command (`SteeringFormats.ts`'s
|
|
207161
|
+
* `seededValidateCommand`) invokes as a leaf step; it does no formatting
|
|
207162
|
+
* in-place — an emitted script has no template context to format against.
|
|
207163
|
+
*
|
|
207164
|
+
* An absent file mirrors `gtd validate`'s own absent-file behavior: exit 0,
|
|
207165
|
+
* no output in plain mode, `{valid: true, errors: []}` under `--json`. A
|
|
207166
|
+
* clean parse is the same shape. A non-clean parse writes `--json`'s
|
|
207167
|
+
* `{valid: false, errors}` body (or each finding, one per line, in plain
|
|
207168
|
+
* mode) BEFORE failing the Effect. In PLAIN mode the failure then carries only
|
|
207169
|
+
* a summary message, landing on stderr alone (`cliErrorLine`'s line) — the
|
|
207170
|
+
* findings are already on stdout. Under `--json`, `Cli.ts`'s single envelope
|
|
207171
|
+
* writer (`report`) ALSO puts its own `{state:"error",prompt}` object on
|
|
207172
|
+
* stdout for any failing Effect, so a `--json` invocation with findings emits
|
|
207173
|
+
* stdout as two newline-delimited JSON documents — this handler's own
|
|
207174
|
+
* `{valid,errors}` body first, then the generic error envelope — rather than
|
|
207175
|
+
* one; a consumer must read line-delimited JSON, not `JSON.parse(stdout)`
|
|
207176
|
+
* whole. Kept as the pragmatic resolution of a real tension in this command's
|
|
207177
|
+
* two governing requirements ("exits non-zero" and "`--json` reports
|
|
207178
|
+
* `{valid,errors}`"): the alternative was piping a `--json` failure through a
|
|
207179
|
+
* NEW exit-code channel bypassing `Cli.ts`'s shared envelope entirely, which
|
|
207180
|
+
* every other command's `--json` failure goes through today.
|
|
207181
|
+
*/
|
|
207182
|
+
const runCheckCommand = (mode, file, json, write) => gen(function* () {
|
|
207183
|
+
const format = steeringFormatFor(mode);
|
|
207184
|
+
if (format === void 0) return yield* fail$2(/* @__PURE__ */ new Error(`gtd check: unknown mode "${mode}" — known modes: ${builtInModeNames().join(", ")}`));
|
|
207185
|
+
const fs = yield* FileSystem;
|
|
207186
|
+
const toError = (e) => e instanceof Error ? e : new Error(String(e));
|
|
207187
|
+
const errors = (yield* fs.exists(file).pipe(mapError$1(toError))) ? format.validate(yield* fs.readFileString(file).pipe(mapError$1(toError))) : [];
|
|
206626
207188
|
if (errors.length === 0) {
|
|
206627
207189
|
if (json) write(JSON.stringify({
|
|
206628
|
-
state: rest.state,
|
|
206629
|
-
file,
|
|
206630
|
-
mode: rest.stateDef.mode,
|
|
206631
207190
|
valid: true,
|
|
206632
207191
|
errors: []
|
|
206633
207192
|
}) + "\n");
|
|
206634
|
-
else write(`${file}: valid\n`);
|
|
206635
207193
|
return;
|
|
206636
207194
|
}
|
|
206637
|
-
|
|
207195
|
+
if (json) write(JSON.stringify({
|
|
207196
|
+
valid: false,
|
|
207197
|
+
errors
|
|
207198
|
+
}) + "\n");
|
|
207199
|
+
else write(errors.join("\n") + "\n");
|
|
207200
|
+
return yield* fail$2(/* @__PURE__ */ new Error(`gtd check: ${file} is not valid under mode "${mode}" (${errors.length} finding(s))`));
|
|
206638
207201
|
});
|
|
206639
207202
|
/** Which declared `on` pattern (if any) each pending change matches — the pure computation `gtd status` reports (both plain and `--json`). `onEdges` is ALREADY RENDERED against `it.vars` (`renderOnEdges`) — the reported pattern is the one a real `gtd step` would match against. */
|
|
206640
207203
|
const computeStatusChanges = (onEdges, changes) => changes.map((change) => {
|
|
@@ -206841,7 +207404,8 @@ const assertInitLocation = (git, fs) => gen(function* () {
|
|
|
206841
207404
|
const needsOf = (kind) => {
|
|
206842
207405
|
switch (kind) {
|
|
206843
207406
|
case "lsp": return "none";
|
|
206844
|
-
case "init":
|
|
207407
|
+
case "init":
|
|
207408
|
+
case "check": return "fs";
|
|
206845
207409
|
case "visualize": return "config";
|
|
206846
207410
|
default: return "state";
|
|
206847
207411
|
}
|
|
@@ -206859,29 +207423,22 @@ const dispatchCommand = (command, json, write) => {
|
|
|
206859
207423
|
case "next": return runNextCommand(json, write);
|
|
206860
207424
|
case "status": return runStatusCommand(json, write);
|
|
206861
207425
|
case "validate": return runValidateCommand(json, write);
|
|
207426
|
+
case "check": return runCheckCommand(command.mode, command.file, json, write);
|
|
206862
207427
|
}
|
|
206863
207428
|
};
|
|
206864
207429
|
/**
|
|
206865
|
-
* Runs `command` inside the bracket every state-touching command shares: the
|
|
206866
|
-
* repo-root guard, then close-any-open-review-window before the command sees
|
|
206867
|
-
* HEAD, then re-arm it afterward whether `command` succeeded or refused (see
|
|
206868
|
-
* `closeReviewWindow`/`openReviewWindow`).
|
|
206869
|
-
*/
|
|
206870
|
-
const runInReviewWindowBracket = (git, fs, command) => gen(function* () {
|
|
206871
|
-
yield* assertRunningFromRepoRoot(git, fs);
|
|
206872
|
-
yield* closeReviewWindow;
|
|
206873
|
-
const outcome = yield* either(command);
|
|
206874
|
-
const rearm = yield* either(openReviewWindow);
|
|
206875
|
-
if (isLeft(outcome)) return yield* fail$2(outcome.left);
|
|
206876
|
-
if (isLeft(rearm)) return yield* fail$2(rearm.left);
|
|
206877
|
-
});
|
|
206878
|
-
/**
|
|
206879
207430
|
* The one entry point `Cli.ts`'s `runCli` calls for a resolved `Command`:
|
|
206880
207431
|
* dispatches to the matching `run*Command` handler (see `dispatchCommand`),
|
|
206881
|
-
* wrapped in the repo-root guard
|
|
206882
|
-
*
|
|
206883
|
-
*
|
|
206884
|
-
*
|
|
207432
|
+
* wrapped in the repo-root guard exactly when `needsOf(command.kind) ===
|
|
207433
|
+
* "state"` — `lsp`/`init`/`visualize`/`check` (the `standaloneKinds`) run
|
|
207434
|
+
* bare. `Cli.ts` has already validated every field on `command` (arity, flag
|
|
207435
|
+
* scope, decoding), so nothing here re-parses argv.
|
|
207436
|
+
*
|
|
207437
|
+
* No review-window close/open bracket runs here any more: every state-command
|
|
207438
|
+
* handler (see `stepAsActor`) now decides for itself, off `ReviewWindow.ts`'s
|
|
207439
|
+
* pure `decideCloseWindow`/`decideOpenWindow`, whether a close/open belongs in
|
|
207440
|
+
* the script it emits — there is no in-process HEAD to rewind ahead of time
|
|
207441
|
+
* (gtd itself no longer writes git for a state command at all).
|
|
206885
207442
|
*/
|
|
206886
207443
|
const runCommand = (command, json, write) => {
|
|
206887
207444
|
const dispatch = dispatchCommand(command, json, write);
|
|
@@ -206889,7 +207446,8 @@ const runCommand = (command, json, write) => {
|
|
|
206889
207446
|
return gen(function* () {
|
|
206890
207447
|
const git = yield* GitService;
|
|
206891
207448
|
const fs = yield* FileSystem;
|
|
206892
|
-
yield*
|
|
207449
|
+
yield* assertRunningFromRepoRoot(git, fs);
|
|
207450
|
+
yield* dispatch;
|
|
206893
207451
|
});
|
|
206894
207452
|
};
|
|
206895
207453
|
//#endregion
|
|
@@ -207089,9 +207647,14 @@ const COMMAND_ROWS = [
|
|
|
207089
207647
|
kind: "validate",
|
|
207090
207648
|
arity: "none",
|
|
207091
207649
|
details: [
|
|
207092
|
-
"
|
|
207093
|
-
"
|
|
207094
|
-
"
|
|
207650
|
+
"Print the script that formats (when declared) then",
|
|
207651
|
+
"validates the resolved rest's steering file, using its",
|
|
207652
|
+
"mode's commands (its file:/mode:), instead of running it —",
|
|
207653
|
+
"a driver runs the script and reads the findings from its",
|
|
207654
|
+
"own exit code/output. Always exits 0; --json emits",
|
|
207655
|
+
"{state, file?, mode?, script} (script is \"\" when there is",
|
|
207656
|
+
"nothing to validate; plain text prints \"nothing to",
|
|
207657
|
+
"validate\" in that case)"
|
|
207095
207658
|
]
|
|
207096
207659
|
},
|
|
207097
207660
|
{
|
|
@@ -207109,6 +207672,20 @@ const COMMAND_ROWS = [
|
|
|
207109
207672
|
"local web server (--port <n>, --no-open; --json prints the",
|
|
207110
207673
|
"model and exits)"
|
|
207111
207674
|
]
|
|
207675
|
+
},
|
|
207676
|
+
{
|
|
207677
|
+
token: "check",
|
|
207678
|
+
kind: "check",
|
|
207679
|
+
arity: { names: ["mode", "file"] },
|
|
207680
|
+
details: [
|
|
207681
|
+
"Read <file> and run the built-in steering format named",
|
|
207682
|
+
"<mode> (see `gtd validate`'s modes: qa, review) over its",
|
|
207683
|
+
"contents, printing each finding one per line and exiting",
|
|
207684
|
+
"non-zero when there are any. Resolves no workflow state and",
|
|
207685
|
+
"reads no config — standalone, runnable from any directory",
|
|
207686
|
+
"with <mode>/<file> given explicitly. This is what a",
|
|
207687
|
+
"workflow's emitted validation script invokes as a leaf step"
|
|
207688
|
+
]
|
|
207112
207689
|
}
|
|
207113
207690
|
];
|
|
207114
207691
|
const commandByToken = (token) => COMMAND_ROWS.find((r) => r.token === token);
|
|
@@ -207147,7 +207724,7 @@ const flagHeader = (row) => {
|
|
|
207147
207724
|
};
|
|
207148
207725
|
const renderHelp = () => {
|
|
207149
207726
|
const commandBlocks = COMMAND_ROWS.map((row) => {
|
|
207150
|
-
const header = row.arity === "none" ? row.token : `${row.token} <${row.arity.name}>`;
|
|
207727
|
+
const header = row.arity === "none" ? row.token : "names" in row.arity ? `${row.token} <${row.arity.names[0]}> <${row.arity.names[1]}>` : `${row.token} <${row.arity.name}>`;
|
|
207151
207728
|
return renderBlock(header, row.details);
|
|
207152
207729
|
});
|
|
207153
207730
|
return `Usage: gtd [command] [options]\n\nCommands:\n${PRELUDE_LOOP + commandBlocks[0] + commandBlocks[1] + ENTRY_SHORT_FORM + commandBlocks.slice(2).join("") + VERSION_BLOCK + HELP_BLOCK}\nOptions:\n${FLAGS.map((row) => renderBlock(flagHeader(row), row.help)).join("")}${renderBlock("--version, -v", ["Print version and exit"]) + renderBlock("--help, -h", ["Print this help and exit"])}`;
|
|
@@ -207233,6 +207810,12 @@ const scopeViolation = (kind, present) => {
|
|
|
207233
207810
|
};
|
|
207234
207811
|
const arityError = (cmd, rest, arity) => {
|
|
207235
207812
|
if (arity === "none") return rest.length > 0 ? `gtd ${cmd}: too many arguments — expected none, got: ${rest.join(", ")}` : void 0;
|
|
207813
|
+
if ("names" in arity) {
|
|
207814
|
+
const [first, second] = arity.names;
|
|
207815
|
+
if (rest.length < 2) return rest.length === 0 ? `gtd ${cmd}: missing ${first} and ${second} arguments` : `gtd ${cmd}: missing ${second} argument`;
|
|
207816
|
+
if (rest.length > 2) return `gtd ${cmd}: too many arguments — expected ${first} and ${second}, got: ${rest.join(", ")}`;
|
|
207817
|
+
return;
|
|
207818
|
+
}
|
|
207236
207819
|
if (rest.length === 0) return `gtd ${cmd}: missing ${arity.name} argument`;
|
|
207237
207820
|
if (rest.length > 1) return `gtd ${cmd}: too many arguments — expected one ${arity.name}, got: ${rest.join(", ")}`;
|
|
207238
207821
|
};
|
|
@@ -207339,6 +207922,15 @@ const parseArgv = (argv) => {
|
|
|
207339
207922
|
},
|
|
207340
207923
|
json
|
|
207341
207924
|
};
|
|
207925
|
+
if (kind === "check") return {
|
|
207926
|
+
kind: "command",
|
|
207927
|
+
command: {
|
|
207928
|
+
kind: "check",
|
|
207929
|
+
mode: restPositionals[0],
|
|
207930
|
+
file: restPositionals[1]
|
|
207931
|
+
},
|
|
207932
|
+
json
|
|
207933
|
+
};
|
|
207342
207934
|
return {
|
|
207343
207935
|
kind: "command",
|
|
207344
207936
|
command: { kind },
|