@pmelab/gtd 63.0.0 → 64.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +4 -3
- package/dist/chunks/{flows-DENt0uWV.mjs → flows-B7ZRCe-B.mjs} +1 -1
- package/dist/chunks/{rolldown-runtime-8H4AJuhK.mjs → rolldown-runtime-D7D4PA-g.mjs} +0 -1
- package/dist/chunks/{steering-DF_BMadH.mjs → steering-DkA51qDa.mjs} +1 -1
- package/dist/chunks/{testkit-DwypA7cH.mjs → testkit-zGjNeQNL.mjs} +1011 -313
- package/dist/flows.mjs +1 -1
- package/dist/gtd.bundle.mjs +3890 -1294
- package/dist/gtd.core.mjs +119 -118
- package/dist/steering.mjs +1 -1
- package/dist/testkit/gherkin.mjs +2 -2
- package/dist/testkit.mjs +2 -2
- package/dist/workflow-kit.mjs +2 -2
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +5 -8
- package/src/cli/Cli.ts +9 -27
- package/src/cli/Output.ts +2 -1
- package/src/cli/Program.test.ts +55 -53
- package/src/cli/Program.ts +11 -30
- package/src/cli/ToolCommands.ts +3 -4
- package/src/edge/Edge.test.ts +38 -12
- package/src/edge/Edge.ts +30 -10
- package/src/emit/SteeringMode.test.ts +4 -2
- package/src/emit/SteeringMode.ts +10 -9
- package/src/gherkin/messages.test.ts +1 -1
- package/src/gherkin/module-config.feature +1 -1
- package/src/gherkin/testkit.feature +2 -2
- package/src/install/Version.ts +14 -13
- package/src/platform/Host.ts +75 -22
- package/src/platform/NodePinFile.ts +44 -0
- package/src/platform/NodeRuntime.ts +19 -0
- package/src/platform/NodeSync.ts +64 -0
- package/src/platform/PinFile.ts +0 -50
- package/src/platform/Sha.ts +121 -0
- package/src/platform/SyncIo.ts +35 -0
- package/src/platform/Workspace.test.ts +8 -3
- package/src/platform/Workspace.ts +82 -123
- package/src/platform/WorktreeState.test.ts +8 -4
- package/src/platform/WorktreeState.ts +21 -19
- package/src/platform/index.ts +18 -5
- package/src/replay/Format.test.ts +16 -13
- package/src/replay/Format.ts +4 -2
- package/src/replay/Replay.test.ts +25 -5
- package/src/replay/Replay.ts +10 -6
- package/src/replay/Trailers.test.ts +10 -10
- package/src/replay/Trailers.ts +23 -1
- package/src/replay/index.ts +1 -0
- package/src/step/planStart.test.ts +2 -2
- package/src/step/planStep.test.ts +4 -4
- package/src/testkit/TestRepo.test.ts +1 -1
- package/src/testkit/TestRepo.ts +4 -18
- package/src/twins/Host.test.ts +151 -0
- package/src/twins/Layers.test.ts +2 -2
- package/src/twins/Layers.ts +44 -18
- package/src/twins/Path.test.ts +52 -0
- package/src/twins/SyncIo.test.ts +153 -0
- package/src/twins/SyncIo.ts +159 -0
- package/src/twins/cliIo.ts +8 -1
- package/src/twins/index.ts +5 -0
- package/src/workflow/discovery.test.ts +6 -5
- package/src/workflow/discovery.ts +43 -32
- package/src/workflow/fingerprint.test.ts +23 -3
- package/src/workflow/fingerprint.ts +9 -11
- package/src/workflow/load.test.ts +16 -6
- package/src/workflow/load.ts +107 -69
- package/src/workflow/pin.test.ts +3 -2
- package/src/workflow/pin.ts +17 -11
package/dist/steering.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as parseThreads, C as FREE_TEXT_PLACEHOLDER, D as findingEntries, E as NOISE_HEADING, N as formatFinding, O as openThreadFindings, S as unansweredNotes, T as FIX_BOX, _ as reviewChunks, a as sections, b as reviewRisks, c as openThreads, d as freeFormFormat, f as applySteeringEdits, g as REVIEW_SECTIONS, h as ANSWER_WONT_FIX, i as sectionBodies, j as parseThreadsWithFindings, k as parseFootnotes, l as steeringFormatFor, m as ANSWER_LEAD, n as stripCodeThreads, o as threads, p as ANSWER_FIX, r as openQuestions, t as parseCodeThreads, v as reviewDecisions, w as isAnswered, x as reviewSections, y as reviewNotes } from "./chunks/steering-
|
|
1
|
+
import { A as parseThreads, C as FREE_TEXT_PLACEHOLDER, D as findingEntries, E as NOISE_HEADING, N as formatFinding, O as openThreadFindings, S as unansweredNotes, T as FIX_BOX, _ as reviewChunks, a as sections, b as reviewRisks, c as openThreads, d as freeFormFormat, f as applySteeringEdits, g as REVIEW_SECTIONS, h as ANSWER_WONT_FIX, i as sectionBodies, j as parseThreadsWithFindings, k as parseFootnotes, l as steeringFormatFor, m as ANSWER_LEAD, n as stripCodeThreads, o as threads, p as ANSWER_FIX, r as openQuestions, t as parseCodeThreads, v as reviewDecisions, w as isAnswered, x as reviewSections, y as reviewNotes } from "./chunks/steering-DkA51qDa.mjs";
|
|
2
2
|
export { ANSWER_FIX, ANSWER_LEAD, ANSWER_WONT_FIX, FIX_BOX, FREE_TEXT_PLACEHOLDER, NOISE_HEADING, REVIEW_SECTIONS, applySteeringEdits, findingEntries, formatFinding, freeFormFormat, isAnswered, openQuestions, openThreadFindings, openThreads, parseCodeThreads, parseFootnotes, parseThreads, parseThreadsWithFindings, reviewChunks, reviewDecisions, reviewNotes, reviewRisks, reviewSections, sectionBodies, sections, steeringFormatFor, stripCodeThreads, threads, unansweredNotes };
|
package/dist/testkit/gherkin.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as runsItsScript, r as testRepo } from "../chunks/testkit-
|
|
2
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { n as runsItsScript, r as testRepo } from "../chunks/testkit-zGjNeQNL.mjs";
|
|
3
2
|
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
4
|
import { After, AfterAll, Given, QuickPickleWorld, Then, When, setWorldConstructor } from "quickpickle";
|
|
5
5
|
import assert from "node:assert";
|
|
6
6
|
//#region src/gherkin/messages.ts
|
package/dist/testkit.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { H as installContext } from "./chunks/flows-
|
|
2
|
-
import { a as verdictWhen, i as verdict, n as runsItsScript, o as whenPrompted, r as testRepo, t as inTurn } from "./chunks/testkit-
|
|
1
|
+
import { H as installContext } from "./chunks/flows-B7ZRCe-B.mjs";
|
|
2
|
+
import { a as verdictWhen, i as verdict, n as runsItsScript, o as whenPrompted, r as testRepo, t as inTurn } from "./chunks/testkit-zGjNeQNL.mjs";
|
|
3
3
|
export { inTurn, installContext, runsItsScript, testRepo, verdict, verdictWhen, whenPrompted };
|
package/dist/workflow-kit.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { B as head, F as changesSince, G as mark, P as changes, X as read, Z as refuse, d as hasThreadFor, et as scope, g as requireReplies, v as requireThreadsClosed } from "./chunks/flows-
|
|
2
|
-
import { A as parseThreads, S as unansweredNotes, b as reviewRisks, n as stripCodeThreads, v as reviewDecisions, y as reviewNotes } from "./chunks/steering-
|
|
1
|
+
import { B as head, F as changesSince, G as mark, P as changes, X as read, Z as refuse, d as hasThreadFor, et as scope, g as requireReplies, v as requireThreadsClosed } from "./chunks/flows-B7ZRCe-B.mjs";
|
|
2
|
+
import { A as parseThreads, S as unansweredNotes, b as reviewRisks, n as stripCodeThreads, v as reviewDecisions, y as reviewNotes } from "./chunks/steering-DkA51qDa.mjs";
|
|
3
3
|
//#region src/workflow-kit/health.ts
|
|
4
4
|
/** Hand a red suite that repeated attempts could not fix to a person, until `limit` rounds are spent. */
|
|
5
5
|
const escalation = async (count, steps) => {
|
package/package.json
CHANGED
package/src/cli/Cli.test.ts
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
import { EXIT_CODES, EXIT_RUNTIME_ERROR, EXIT_USAGE_ERROR } from "../wire/index.js"
|
|
26
26
|
import { Host } from "../platform/index.js"
|
|
27
27
|
import { type CommandRequirements } from "./Cli.js"
|
|
28
|
-
import { InMemRepo, makeCapturingCliIo, testLayers } from "../twins/index.js"
|
|
28
|
+
import { InMemRepo, inMemoryHost, makeCapturingCliIo, testLayers } from "../twins/index.js"
|
|
29
29
|
|
|
30
30
|
const FLAG_NAMES = [
|
|
31
31
|
"--json",
|
|
@@ -1342,13 +1342,10 @@ describe("runCli — a garbled GTD_FLAG_* value", () => {
|
|
|
1342
1342
|
it("exits 1 naming the variable", async () => {
|
|
1343
1343
|
const { io, captured } = capturingIo(
|
|
1344
1344
|
() =>
|
|
1345
|
-
Layer.succeed(
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
realPath: (p: string) => Effect.succeed(p),
|
|
1350
|
-
env: { GTD_FLAG_BETA: "ture" },
|
|
1351
|
-
}) as unknown as Layer.Layer<CommandRequirements>,
|
|
1345
|
+
Layer.succeed(
|
|
1346
|
+
Host,
|
|
1347
|
+
inMemoryHost("/", "/", { GTD_FLAG_BETA: "ture" }),
|
|
1348
|
+
) as unknown as Layer.Layer<CommandRequirements>,
|
|
1352
1349
|
)
|
|
1353
1350
|
await Effect.runPromise(
|
|
1354
1351
|
runCli(["node", "gtd.js", "next"], {
|
package/src/cli/Cli.ts
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
Workspace,
|
|
10
10
|
CommandRunner,
|
|
11
11
|
NodeServices,
|
|
12
|
+
nodeStdio,
|
|
13
|
+
type Stdio,
|
|
12
14
|
} from "../platform/index.js"
|
|
13
15
|
import {
|
|
14
16
|
flagSettings,
|
|
@@ -1334,10 +1336,7 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1334
1336
|
// runCli
|
|
1335
1337
|
// ---------------------------------------------------------------------------
|
|
1336
1338
|
|
|
1337
|
-
export interface CliIo {
|
|
1338
|
-
readonly stdout: (chunk: string | Uint8Array) => void
|
|
1339
|
-
readonly stderr: (chunk: string) => void
|
|
1340
|
-
readonly exit: (code: number) => void
|
|
1339
|
+
export interface CliIo extends Stdio {
|
|
1341
1340
|
/**
|
|
1342
1341
|
* Deferred thunk — a layer is built only for a resolved `Command`, never
|
|
1343
1342
|
* for `--version`/`--help`/a usage error. `verbose` (`--verbose`/`-v`,
|
|
@@ -1349,13 +1348,10 @@ export interface CliIo {
|
|
|
1349
1348
|
readonly flags?: readonly FlagDeclaration[]
|
|
1350
1349
|
}
|
|
1351
1350
|
|
|
1352
|
-
//
|
|
1353
|
-
//
|
|
1354
|
-
//
|
|
1355
|
-
|
|
1356
|
-
const writeStderr = (chunk: string): void => {
|
|
1357
|
-
process.stderr.write(chunk)
|
|
1358
|
-
}
|
|
1351
|
+
// `nodeCliIo` hands this one sink to both `stderr` (errors) and `layers`'s
|
|
1352
|
+
// `Narrator` (narration), so a real invocation's narration and its
|
|
1353
|
+
// remediation interleave on the same fd exactly as written.
|
|
1354
|
+
const stdio = nodeStdio()
|
|
1359
1355
|
|
|
1360
1356
|
export type LiveToolCommands = Layer.Layer<
|
|
1361
1357
|
ToolCommands,
|
|
@@ -1364,21 +1360,7 @@ export type LiveToolCommands = Layer.Layer<
|
|
|
1364
1360
|
>
|
|
1365
1361
|
|
|
1366
1362
|
export const nodeCliIo = (tools: LiveToolCommands): CliIo => ({
|
|
1367
|
-
|
|
1368
|
-
// socket — passing a completion callback (and never force-closing, see
|
|
1369
|
-
// `exit` below) keeps a queued remainder from being discarded, so a large
|
|
1370
|
-
// artifact still reaches a slow reader even after a non-zero exit.
|
|
1371
|
-
stdout: (chunk) => {
|
|
1372
|
-
process.stdout.write(chunk, () => {})
|
|
1373
|
-
},
|
|
1374
|
-
stderr: writeStderr,
|
|
1375
|
-
exit: (code) => {
|
|
1376
|
-
// Never `process.exit(code)`: that tears the process down out from under
|
|
1377
|
-
// an undrained `stdout.write` above and truncates the artifact mid-pipe.
|
|
1378
|
-
// Setting `exitCode` lets the event loop run to completion — draining
|
|
1379
|
-
// any queued write — before Node exits gracefully with this code.
|
|
1380
|
-
process.exitCode = code
|
|
1381
|
-
},
|
|
1363
|
+
...stdio,
|
|
1382
1364
|
// A Layer is a lazy description, not a running service — building this
|
|
1383
1365
|
// object touches no filesystem/git; only `Effect.provide` running the
|
|
1384
1366
|
// result does. `runCli` calls this thunk exactly when `plan.kind ===
|
|
@@ -1394,7 +1376,7 @@ export const nodeCliIo = (tools: LiveToolCommands): CliIo => ({
|
|
|
1394
1376
|
ConfigDiscovery.Live,
|
|
1395
1377
|
EngineCore.Live,
|
|
1396
1378
|
Workspace.Live,
|
|
1397
|
-
Narrator.layer(
|
|
1379
|
+
Narrator.layer(stdio.stderr, verbose),
|
|
1398
1380
|
CommandRunner.Live,
|
|
1399
1381
|
).pipe(
|
|
1400
1382
|
Layer.provideMerge(GitService.Live),
|
package/src/cli/Output.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect } from "effect"
|
|
1
|
+
import { Effect, type Path } from "effect"
|
|
2
2
|
import { CommandRunner, GitService, Host, Narrator, Workspace } from "../platform/index.js"
|
|
3
3
|
import {
|
|
4
4
|
renderScriptJson,
|
|
@@ -70,6 +70,7 @@ export type CommandRequirements =
|
|
|
70
70
|
| EngineCore
|
|
71
71
|
| Workspace
|
|
72
72
|
| Host
|
|
73
|
+
| Path.Path
|
|
73
74
|
| Narrator
|
|
74
75
|
| CommandRunner
|
|
75
76
|
| ToolCommands
|
package/src/cli/Program.test.ts
CHANGED
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Cause, Context, Effect, Exit } from "effect"
|
|
8
|
-
import {
|
|
9
|
-
import { afterEach, describe, expect, it, vi } from "vitest"
|
|
8
|
+
import { describe, expect, it, vi } from "vitest"
|
|
10
9
|
|
|
11
10
|
// `gtd ui`'s bind-host lookup reads the real `os.networkInterfaces()` — a host
|
|
12
11
|
// on a tailnet would make the "no Tailscale interface" refusal below fail.
|
|
@@ -24,6 +23,8 @@ import {
|
|
|
24
23
|
testLayers,
|
|
25
24
|
applyEmittedScript,
|
|
26
25
|
takePinBeat,
|
|
26
|
+
IN_MEMORY_PID,
|
|
27
|
+
IN_MEMORY_SCRATCH,
|
|
27
28
|
} from "../twins/index.js"
|
|
28
29
|
import {
|
|
29
30
|
commitAll,
|
|
@@ -110,7 +111,7 @@ export const hotfix = workflow(
|
|
|
110
111
|
expect(exitCode).toBe(0)
|
|
111
112
|
expect(stdout).toContain(
|
|
112
113
|
commitAll(
|
|
113
|
-
`gtd(human): working\n\nGtd-Workflow: hotfix\nGtd-Var: greeting=hello\nGtd-Format:
|
|
114
|
+
`gtd(human): working\n\nGtd-Workflow: hotfix\nGtd-Var: greeting=hello\nGtd-Format: 4`,
|
|
114
115
|
),
|
|
115
116
|
)
|
|
116
117
|
expect(repo.commitHistory()).toHaveLength(before)
|
|
@@ -197,7 +198,7 @@ export const hotfix = workflow(
|
|
|
197
198
|
expect(applyEmittedScript(repo, new Map(), stdout).ok).toBe(true)
|
|
198
199
|
expect(repo.lastCommitSubject()).toBe("gtd(human): working")
|
|
199
200
|
const pin = JSON.parse((await run(repo, "next", "--json")).stdout) as Record<string, unknown>
|
|
200
|
-
expect(pin).toMatchObject({ kind: "script", state: "pin", actor: "check", pin: "add" })
|
|
201
|
+
expect(pin).toMatchObject({ kind: "script", state: "gtd:pin", actor: "check", pin: "add" })
|
|
201
202
|
expect(await takePinBeat(repo, "add")).toBe(true)
|
|
202
203
|
const next = JSON.parse((await run(repo, "next", "--json")).stdout) as Record<string, unknown>
|
|
203
204
|
expect(next.kind).toBe("prompt")
|
|
@@ -673,7 +674,7 @@ describe("gtd validate — the mode-contradiction round-trip", () => {
|
|
|
673
674
|
const { stdout, exitCode } = await runEnv(repo, { TMPDIR: "/fixture-scratch" }, "validate")
|
|
674
675
|
expect(exitCode).toBe(0)
|
|
675
676
|
|
|
676
|
-
const samplePath = `/fixture-scratch/gtd-mode-sample-qa-${
|
|
677
|
+
const samplePath = `/fixture-scratch/gtd-mode-sample-qa-${IN_MEMORY_PID}.md`
|
|
677
678
|
const roundTripIndex = stdout.indexOf(`printf '%s' `)
|
|
678
679
|
const guardIndex = stdout.indexOf(`[ -f '.gtd/PLAN.md' ] || exit 0`)
|
|
679
680
|
// The real format command, distinct from the round-trip's own copy
|
|
@@ -724,11 +725,11 @@ describe("gtd validate — the mode-contradiction round-trip", () => {
|
|
|
724
725
|
expect(stdout).not.toContain("skipping")
|
|
725
726
|
})
|
|
726
727
|
|
|
727
|
-
it("
|
|
728
|
+
it("falls back to the host's own scratch dir when TMPDIR is unset", async () => {
|
|
728
729
|
const repo = await atWorking(`{ qa: { format: "my-formatter $GTD_FILE" } }`)
|
|
729
730
|
const { stdout, exitCode } = await runEnv(repo, {}, "validate")
|
|
730
731
|
expect(exitCode).toBe(0)
|
|
731
|
-
expect(stdout).toContain(
|
|
732
|
+
expect(stdout).toContain(`${IN_MEMORY_SCRATCH}/gtd-mode-sample-qa-${IN_MEMORY_PID}.md`)
|
|
732
733
|
})
|
|
733
734
|
|
|
734
735
|
it("gtd validate the COMMAND exits 0 even when the emitted SCRIPT would fail if run — gtd only prints it", async () => {
|
|
@@ -791,14 +792,14 @@ describe("gtd next — refuses when history diverges from what replay reaches",
|
|
|
791
792
|
|
|
792
793
|
describe("a history written in a format this gtd does not read", () => {
|
|
793
794
|
// The exact wording is pinned in Format.test.ts; this is the part that proves the format gate fired.
|
|
794
|
-
const FOREIGN = "is written in history format
|
|
795
|
+
const FOREIGN = "is written in history format 5, but this gtd reads formats 1 to 4"
|
|
795
796
|
|
|
796
797
|
/** A step commit that would also diverge, so only the format gate can explain the refusal. */
|
|
797
798
|
const foreignStep = async (): Promise<InMemRepo> => {
|
|
798
799
|
const repo = seed(IDLE_THEN_WORKING)
|
|
799
800
|
await landTurn(repo, { "NOTE.md": "a note\n" })
|
|
800
801
|
repo.writeFile("WORK.md", "done\n")
|
|
801
|
-
repo.seedCommit("gtd(agent): elsewhere → review\n\nGtd-Step: elsewhere#1\nGtd-Format:
|
|
802
|
+
repo.seedCommit("gtd(agent): elsewhere → review\n\nGtd-Step: elsewhere#1\nGtd-Format: 5")
|
|
802
803
|
return repo
|
|
803
804
|
}
|
|
804
805
|
|
|
@@ -823,7 +824,7 @@ describe("a history written in a format this gtd does not read", () => {
|
|
|
823
824
|
it("refuses even when a foreign subject hides the commit from the episode", async () => {
|
|
824
825
|
const repo = seed(IDLE_THEN_WORKING)
|
|
825
826
|
repo.writeFile("NOTE.md", "a note\n")
|
|
826
|
-
repo.seedCommit("gtd2[human] idle -> working\n\nGtd-Format:
|
|
827
|
+
repo.seedCommit("gtd2[human] idle -> working\n\nGtd-Format: 5")
|
|
827
828
|
for (const command of ["next", "abandon"]) {
|
|
828
829
|
const { exitCode, stderr } = await run(repo, command)
|
|
829
830
|
expect(exitCode).toBe(1)
|
|
@@ -834,7 +835,7 @@ describe("a history written in a format this gtd does not read", () => {
|
|
|
834
835
|
it("a commit gtd never wrote is not read for a format, even quoting the trailer", async () => {
|
|
835
836
|
const repo = seed(IDLE_THEN_WORKING)
|
|
836
837
|
repo.writeFile("NOTE.md", "a note\n")
|
|
837
|
-
repo.seedCommit("feat: x\n\nGtd-Format:
|
|
838
|
+
repo.seedCommit("feat: x\n\nGtd-Format: 5")
|
|
838
839
|
expect((await run(repo, "next")).exitCode).toBe(0)
|
|
839
840
|
})
|
|
840
841
|
|
|
@@ -842,7 +843,7 @@ describe("a history written in a format this gtd does not read", () => {
|
|
|
842
843
|
const repo = seed(IDLE_THEN_WORKING)
|
|
843
844
|
const base = repo.resolveRef("HEAD")!
|
|
844
845
|
repo.writeFile("NOTE.md", "a note\n")
|
|
845
|
-
repo.seedCommit("gtd(human): idle → working\n\nGtd-Step: idle#1\nGtd-Format:
|
|
846
|
+
repo.seedCommit("gtd(human): idle → working\n\nGtd-Step: idle#1\nGtd-Format: 5")
|
|
846
847
|
repo.updateRef(HISTORY_REF, repo.resolveRef("HEAD")!)
|
|
847
848
|
repo.hardResetTo(base)
|
|
848
849
|
const { exitCode, stderr } = await run(repo, "restore")
|
|
@@ -1403,7 +1404,7 @@ export default async () => {
|
|
|
1403
1404
|
it("a turn that finishes a pinned flow targets the unpin", async () => {
|
|
1404
1405
|
const repo = await atWorking()
|
|
1405
1406
|
repo.writeFile("REVIEW.md", "an updated review\n")
|
|
1406
|
-
expect(await nextOf(repo)).toMatchObject({ target: "unpin" })
|
|
1407
|
+
expect(await nextOf(repo)).toMatchObject({ target: "gtd:unpin" })
|
|
1407
1408
|
})
|
|
1408
1409
|
|
|
1409
1410
|
it("plain gtd next shows the same preview as a `Next:` line", async () => {
|
|
@@ -1495,7 +1496,7 @@ export default async () => {
|
|
|
1495
1496
|
expect(script).not.toContain("git reset --mixed")
|
|
1496
1497
|
expect(script).not.toContain("nothing to retain")
|
|
1497
1498
|
expect(applyEmittedScript(repo, new Map(), script).ok).toBe(true)
|
|
1498
|
-
expect(repo.lastCommitSubject()).toBe("gtd(check): checking → unpin")
|
|
1499
|
+
expect(repo.lastCommitSubject()).toBe("gtd(check): checking → gtd:unpin")
|
|
1499
1500
|
})
|
|
1500
1501
|
|
|
1501
1502
|
it("the same rest with a pending change lands an ordinary commit too", async () => {
|
|
@@ -1544,7 +1545,7 @@ export default async () => {
|
|
|
1544
1545
|
}
|
|
1545
1546
|
expect(parsed.settled).toBe(false)
|
|
1546
1547
|
expect(parsed.idle).toBe(false)
|
|
1547
|
-
expect(parsed.state).toBe("unpin")
|
|
1548
|
+
expect(parsed.state).toBe("gtd:unpin")
|
|
1548
1549
|
})
|
|
1549
1550
|
|
|
1550
1551
|
it("gtd land --json an ordinary commit reports settled:false", async () => {
|
|
@@ -1693,7 +1694,7 @@ export default async () => {
|
|
|
1693
1694
|
const { exitCode, script } = await landAndApply(repo)
|
|
1694
1695
|
expect(exitCode).toBe(0)
|
|
1695
1696
|
expect(script).not.toContain("fmt-notes")
|
|
1696
|
-
expect(repo.lastCommitSubject("HEAD~1")).toBe("gtd(agent): drafting → unpin")
|
|
1697
|
+
expect(repo.lastCommitSubject("HEAD~1")).toBe("gtd(agent): drafting → gtd:unpin")
|
|
1697
1698
|
})
|
|
1698
1699
|
|
|
1699
1700
|
it("`gtd next --json`'s `validate` field carries the mode's format command", async () => {
|
|
@@ -1973,7 +1974,7 @@ export default async () => {
|
|
|
1973
1974
|
it("refuses once the process has closed and HEAD rests at the initial step again", async () => {
|
|
1974
1975
|
const repo = await atDeciding()
|
|
1975
1976
|
await landTurn(repo)
|
|
1976
|
-
expect(repo.lastCommitSubject()).toBe("gtd(check): unpin → idle")
|
|
1977
|
+
expect(repo.lastCommitSubject()).toBe("gtd(check): gtd:unpin → idle")
|
|
1977
1978
|
|
|
1978
1979
|
const { exitCode, stdout, stderr } = await run(repo, "base")
|
|
1979
1980
|
expect(exitCode).toBe(1)
|
|
@@ -2186,41 +2187,32 @@ export default workflow(
|
|
|
2186
2187
|
expect(first.stdout).toBe(second.stdout)
|
|
2187
2188
|
})
|
|
2188
2189
|
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
const withStdin = async <T>(content: string, fn: () => Promise<T>): Promise<T> => {
|
|
2198
|
-
savedStdin = Object.getOwnPropertyDescriptor(process, "stdin")
|
|
2199
|
-
const stdin = new PassThrough()
|
|
2200
|
-
Object.defineProperty(process, "stdin", { value: stdin, configurable: true })
|
|
2201
|
-
stdin.end(content)
|
|
2202
|
-
try {
|
|
2203
|
-
return await fn()
|
|
2204
|
-
} finally {
|
|
2205
|
-
if (savedStdin) Object.defineProperty(process, "stdin", savedStdin)
|
|
2206
|
-
savedStdin = undefined
|
|
2207
|
-
}
|
|
2190
|
+
const runWithStdin = async (
|
|
2191
|
+
stdin: string,
|
|
2192
|
+
repo: InMemRepo,
|
|
2193
|
+
...args: string[]
|
|
2194
|
+
): Promise<{ readonly stdout: string; readonly stderr: string; readonly exitCode: number }> => {
|
|
2195
|
+
const { io, result } = makeCapturingCliIo(repo, {}, {}, stdin)
|
|
2196
|
+
await Effect.runPromise(runCli(["node", "gtd.js", ...args], io))
|
|
2197
|
+
return result()
|
|
2208
2198
|
}
|
|
2209
2199
|
|
|
2210
2200
|
const VERDICT = JSON.stringify([{ id: "q1", answer: true, p: 0.97 }])
|
|
2211
2201
|
|
|
2212
2202
|
it("gtd judge answer decodes a verdict on stdin against the pending question ids and succeeds", async () => {
|
|
2213
2203
|
const repo = seededRepo()
|
|
2214
|
-
const { exitCode, stdout } = await
|
|
2204
|
+
const { exitCode, stdout } = await runWithStdin(VERDICT, repo, "judge", "answer")
|
|
2215
2205
|
expect(exitCode).toBe(0)
|
|
2216
2206
|
expect(stdout.length).toBeGreaterThan(0)
|
|
2217
2207
|
})
|
|
2218
2208
|
|
|
2219
2209
|
it("gtd judge answer refuses a verdict naming a question id the pending judgment never declared, with the usage-error exit code", async () => {
|
|
2220
2210
|
const repo = seededRepo()
|
|
2221
|
-
const { exitCode, stderr } = await
|
|
2211
|
+
const { exitCode, stderr } = await runWithStdin(
|
|
2222
2212
|
JSON.stringify([{ id: "not-a-real-question", answer: true, p: 0.97 }]),
|
|
2223
|
-
|
|
2213
|
+
repo,
|
|
2214
|
+
"judge",
|
|
2215
|
+
"answer",
|
|
2224
2216
|
)
|
|
2225
2217
|
expect(exitCode).toBe(2)
|
|
2226
2218
|
expect(stderr).toContain("does not match the pending questions")
|
|
@@ -2228,16 +2220,18 @@ export default workflow(
|
|
|
2228
2220
|
|
|
2229
2221
|
it("gtd judge answer refuses when stdin is not valid JSON at all, with the usage-error exit code", async () => {
|
|
2230
2222
|
const repo = seededRepo()
|
|
2231
|
-
const { exitCode, stderr } = await
|
|
2223
|
+
const { exitCode, stderr } = await runWithStdin("not json", repo, "judge", "answer")
|
|
2232
2224
|
expect(exitCode).toBe(2)
|
|
2233
2225
|
expect(stderr).toContain("stdin is not valid JSON")
|
|
2234
2226
|
})
|
|
2235
2227
|
|
|
2236
2228
|
it("gtd judge answer refuses a verdict whose p is above 1 — an unvalidated p would clear every minP", async () => {
|
|
2237
2229
|
const repo = seededRepo()
|
|
2238
|
-
const { exitCode, stderr } = await
|
|
2230
|
+
const { exitCode, stderr } = await runWithStdin(
|
|
2239
2231
|
JSON.stringify([{ id: "q1", answer: true, p: 5 }]),
|
|
2240
|
-
|
|
2232
|
+
repo,
|
|
2233
|
+
"judge",
|
|
2234
|
+
"answer",
|
|
2241
2235
|
)
|
|
2242
2236
|
expect(exitCode).toBe(2)
|
|
2243
2237
|
expect(stderr).toContain("does not match the pending questions")
|
|
@@ -2245,9 +2239,11 @@ export default workflow(
|
|
|
2245
2239
|
|
|
2246
2240
|
it("gtd judge answer refuses a verdict whose p is negative", async () => {
|
|
2247
2241
|
const repo = seededRepo()
|
|
2248
|
-
const { exitCode, stderr } = await
|
|
2242
|
+
const { exitCode, stderr } = await runWithStdin(
|
|
2249
2243
|
JSON.stringify([{ id: "q1", answer: true, p: -0.1 }]),
|
|
2250
|
-
|
|
2244
|
+
repo,
|
|
2245
|
+
"judge",
|
|
2246
|
+
"answer",
|
|
2251
2247
|
)
|
|
2252
2248
|
expect(exitCode).toBe(2)
|
|
2253
2249
|
expect(stderr).toContain("does not match the pending questions")
|
|
@@ -2255,15 +2251,19 @@ export default workflow(
|
|
|
2255
2251
|
|
|
2256
2252
|
it("gtd judge answer refuses through the ordinary error envelope when the rest is no judge step", async () => {
|
|
2257
2253
|
const repo = seededRepoWithoutJudge()
|
|
2258
|
-
const { exitCode, stderr } = await
|
|
2254
|
+
const { exitCode, stderr } = await runWithStdin("[]", repo, "judge", "answer")
|
|
2259
2255
|
expect(exitCode).toBe(1)
|
|
2260
2256
|
expect(stderr).toContain('declares no "judge:"')
|
|
2261
2257
|
})
|
|
2262
2258
|
|
|
2263
2259
|
it("gtd judge answer --json=script emits a POSIX sh script carrying a Gtd-Judge: trailer on the step commit", async () => {
|
|
2264
2260
|
const repo = seededLandingRepo()
|
|
2265
|
-
const { stdout, exitCode } = await
|
|
2266
|
-
|
|
2261
|
+
const { stdout, exitCode } = await runWithStdin(
|
|
2262
|
+
VERDICT,
|
|
2263
|
+
repo,
|
|
2264
|
+
"judge",
|
|
2265
|
+
"answer",
|
|
2266
|
+
"--json=script",
|
|
2267
2267
|
)
|
|
2268
2268
|
expect(exitCode).toBe(0)
|
|
2269
2269
|
expect(stdout).toContain(DID_NOT_RUN_COMMENT)
|
|
@@ -2276,16 +2276,14 @@ export default workflow(
|
|
|
2276
2276
|
|
|
2277
2277
|
it("gtd judge answer --json=script opens with the same leading comment as land's script", async () => {
|
|
2278
2278
|
const repo = seededLandingRepo()
|
|
2279
|
-
const { stdout: script } = await
|
|
2280
|
-
run(repo, "judge", "answer", "--json=script"),
|
|
2281
|
-
)
|
|
2279
|
+
const { stdout: script } = await runWithStdin(VERDICT, repo, "judge", "answer", "--json=script")
|
|
2282
2280
|
const { stdout: landScript } = await run(repo, "land", "--json=script")
|
|
2283
2281
|
expect(script.split("\n")[0]).toBe(landScript.split("\n")[0])
|
|
2284
2282
|
})
|
|
2285
2283
|
|
|
2286
2284
|
it("plain gtd judge answer (no --json) names the commit and points at --json=script, never the script itself", async () => {
|
|
2287
2285
|
const repo = seededLandingRepo()
|
|
2288
|
-
const { stdout, exitCode } = await
|
|
2286
|
+
const { stdout, exitCode } = await runWithStdin(VERDICT, repo, "judge", "answer")
|
|
2289
2287
|
expect(exitCode).toBe(0)
|
|
2290
2288
|
expect(stdout).toContain("--json=script")
|
|
2291
2289
|
expect(stdout).not.toContain(DID_NOT_RUN_COMMENT)
|
|
@@ -2305,8 +2303,12 @@ export default workflow(
|
|
|
2305
2303
|
|
|
2306
2304
|
it("gtd judge answer records no truncation on the landing commit — replay recomputes the cut from the budget", async () => {
|
|
2307
2305
|
const repo = seededTruncatingLandingRepo("20", "aaaaaaaaaa\nbbbbbbbbbb\ncccccccccc\n")
|
|
2308
|
-
const { stdout, exitCode } = await
|
|
2309
|
-
|
|
2306
|
+
const { stdout, exitCode } = await runWithStdin(
|
|
2307
|
+
VERDICT,
|
|
2308
|
+
repo,
|
|
2309
|
+
"judge",
|
|
2310
|
+
"answer",
|
|
2311
|
+
"--json=script",
|
|
2310
2312
|
)
|
|
2311
2313
|
expect(exitCode).toBe(0)
|
|
2312
2314
|
expect(applyEmittedScript(repo, new Map(), stdout).ok).toBe(true)
|
package/src/cli/Program.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, Option, Schema } from "effect"
|
|
1
|
+
import { Effect, Option, type Path, Schema } from "effect"
|
|
2
2
|
import {
|
|
3
3
|
GtdError,
|
|
4
4
|
GtdUsageError,
|
|
@@ -99,7 +99,7 @@ import {
|
|
|
99
99
|
restoredOutcome,
|
|
100
100
|
} from "../emit/index.js"
|
|
101
101
|
import { GTD_VERSION, renderBriefing, WORKFLOWS_REPO } from "../install/index.js"
|
|
102
|
-
import { ToolCommands
|
|
102
|
+
import { ToolCommands } from "./ToolCommands.js"
|
|
103
103
|
import {
|
|
104
104
|
normalizeScriptNewline,
|
|
105
105
|
SelectorUsageError,
|
|
@@ -210,14 +210,12 @@ export type Command =
|
|
|
210
210
|
*/
|
|
211
211
|
export type Needs = "pure" | "removed" | "none" | "fs" | "state"
|
|
212
212
|
|
|
213
|
-
const selfGtd = (): SelfGtd => {
|
|
214
|
-
const script = process.argv[1]
|
|
215
|
-
return script === undefined ? ["gtd"] : [process.execPath, script]
|
|
216
|
-
}
|
|
217
|
-
|
|
218
213
|
/** `needs: "none"` skips the repo-root guard — the server is keyed on file name, not workflow state. */
|
|
219
|
-
const runLspCommand = (): Effect.Effect<void, Error, ToolCommands> =>
|
|
220
|
-
Effect.
|
|
214
|
+
const runLspCommand = (): Effect.Effect<void, Error, ToolCommands | Host> =>
|
|
215
|
+
Effect.gen(function* () {
|
|
216
|
+
const { self } = yield* Host
|
|
217
|
+
return yield* (yield* ToolCommands).lsp(self)
|
|
218
|
+
})
|
|
221
219
|
|
|
222
220
|
/**
|
|
223
221
|
* Prints the briefing, emitted by the binary itself so it's always current.
|
|
@@ -246,7 +244,7 @@ const runBundleCommand = (
|
|
|
246
244
|
): Effect.Effect<
|
|
247
245
|
void,
|
|
248
246
|
Error,
|
|
249
|
-
ConfigService | ConfigDiscovery | EngineCore | Host | Narrator | Workspace
|
|
247
|
+
ConfigService | ConfigDiscovery | EngineCore | Host | Path.Path | Narrator | Workspace
|
|
250
248
|
> =>
|
|
251
249
|
Effect.gen(function* () {
|
|
252
250
|
yield* (yield* ConfigService).load
|
|
@@ -430,23 +428,6 @@ const parseJudgeDocument = (rendered: string): Effect.Effect<JudgeDocument, Erro
|
|
|
430
428
|
),
|
|
431
429
|
})
|
|
432
430
|
|
|
433
|
-
/**
|
|
434
|
-
* The first stdin-consuming command outside `src/lsp/Lsp.ts` (which reads LSP's
|
|
435
|
-
* own length-prefixed frames off `process.stdin`, not a one-shot read) —
|
|
436
|
-
* reads stdin to completion and decodes it as UTF-8 text, no framing.
|
|
437
|
-
*/
|
|
438
|
-
const readStdin = (): Effect.Effect<string, Error> =>
|
|
439
|
-
Effect.tryPromise({
|
|
440
|
-
try: async () => {
|
|
441
|
-
const chunks: Buffer[] = []
|
|
442
|
-
for await (const chunk of process.stdin as AsyncIterable<Buffer>) {
|
|
443
|
-
chunks.push(chunk)
|
|
444
|
-
}
|
|
445
|
-
return Buffer.concat(chunks).toString("utf8")
|
|
446
|
-
},
|
|
447
|
-
catch: (e) => (e instanceof Error ? e : new Error(String(e))),
|
|
448
|
-
})
|
|
449
|
-
|
|
450
431
|
/** A stdin that is no judgment is the caller's mistake, a usage error like `gtd judge answer`'s. */
|
|
451
432
|
const judgmentShape = (input: string): Effect.Effect<void, GtdUsageError> => {
|
|
452
433
|
let doc: unknown
|
|
@@ -470,7 +451,7 @@ const runJudgeRunCommand = (
|
|
|
470
451
|
const host = yield* Host
|
|
471
452
|
const tools = yield* ToolCommands
|
|
472
453
|
yield* refuseLegacyRc
|
|
473
|
-
const input = yield*
|
|
454
|
+
const input = yield* host.stdin
|
|
474
455
|
yield* judgmentShape(input)
|
|
475
456
|
const verdict = yield* Effect.tryPromise({
|
|
476
457
|
try: () =>
|
|
@@ -535,7 +516,7 @@ const runJudgeAnswerCommand = (
|
|
|
535
516
|
const document = yield* parseJudgeDocument(rendered.judge)
|
|
536
517
|
const ids = document.questions.map((q) => q.id)
|
|
537
518
|
|
|
538
|
-
const raw = yield*
|
|
519
|
+
const raw = yield* (yield* Host).stdin
|
|
539
520
|
const parsed = yield* Effect.try({
|
|
540
521
|
try: () => JSON.parse(raw) as unknown,
|
|
541
522
|
catch: () => new SelectorUsageError("gtd judge answer: stdin is not valid JSON"),
|
|
@@ -946,7 +927,7 @@ const resolveValidateScript = (
|
|
|
946
927
|
): Effect.Effect<
|
|
947
928
|
{ readonly file: string; readonly mode: StateMode; readonly script: string } | undefined,
|
|
948
929
|
Error,
|
|
949
|
-
Host
|
|
930
|
+
Host | Path.Path
|
|
950
931
|
> =>
|
|
951
932
|
Effect.gen(function* () {
|
|
952
933
|
const file = rest.hints.file
|
package/src/cli/ToolCommands.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { Context } from "effect"
|
|
2
2
|
import type { Effect } from "effect"
|
|
3
3
|
import type { UiSettings } from "../flows/index.js"
|
|
4
|
-
|
|
5
|
-
/** How to run THIS gtd — a tool (the LSP) never reads a different version's state than the one it ships with. */
|
|
6
|
-
export type SelfGtd = readonly [string, ...string[]]
|
|
4
|
+
import type { HostOps } from "../platform/index.js"
|
|
7
5
|
|
|
8
6
|
export interface UiRequest {
|
|
9
7
|
readonly flags: {
|
|
@@ -36,7 +34,8 @@ export class ToolCommands extends Context.Service<
|
|
|
36
34
|
request: UiRequest,
|
|
37
35
|
out: { readonly write: (chunk: string) => void; readonly flush: () => void },
|
|
38
36
|
) => Effect.Effect<void, Error>
|
|
39
|
-
|
|
37
|
+
/** `gtd` runs THIS gtd, so the LSP never reads a different version's state than the one it ships with. */
|
|
38
|
+
readonly lsp: (gtd: HostOps["self"]) => Effect.Effect<void, Error>
|
|
40
39
|
readonly judge: (request: JudgeRequest) => Promise<string>
|
|
41
40
|
}
|
|
42
41
|
>()("ToolCommands") {}
|