@pmelab/gtd 60.0.0 → 61.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 +21 -2
- package/dist/chunks/testkit-C9sq0x07.mjs +30758 -0
- package/dist/chunks/{wire-DZ8R1RU9.mjs → wire-CPNSm_9N.mjs} +27 -12
- package/dist/gtd.bundle.mjs +12238 -20706
- package/dist/gtd.core.mjs +593 -0
- package/dist/testkit/gherkin.mjs +2 -2
- package/dist/testkit.mjs +1 -1
- package/dist/wire.mjs +2 -2
- package/package.json +7 -7
- package/src/cli/Cli.test.ts +2 -2
- package/src/cli/Cli.ts +68 -51
- package/src/cli/History.test.ts +10 -1
- package/src/cli/Metrics.test.ts +10 -1
- package/src/cli/Output.test.ts +1 -1
- package/src/cli/Output.ts +3 -2
- package/src/cli/Program.test.ts +44 -25
- package/src/cli/Program.ts +39 -11
- package/src/cli/Strays.ts +3 -2
- package/src/cli/ToolCommands.ts +2 -2
- package/src/edge/Edge.test.ts +86 -31
- package/src/edge/Edge.ts +164 -31
- package/src/emit/GitScript.ts +21 -3
- package/src/emit/index.ts +3 -0
- package/src/flows/runtime.ts +1 -1
- package/src/gherkin/messages.test.ts +1 -0
- package/src/gherkin/module-config.feature +1 -0
- package/src/gherkin/more-steps.feature +1 -0
- package/src/gherkin/testkit.feature +2 -1
- package/src/install/Install.ts +9 -2
- package/src/install/Version.ts +9 -3
- package/src/install/index.ts +1 -1
- package/src/platform/CommandRunner.ts +13 -11
- package/src/platform/Commentary.ts +2 -2
- package/src/platform/Git.test.ts +4 -5
- package/src/platform/Git.ts +42 -38
- package/src/platform/Host.ts +1 -1
- package/src/platform/NodeServices.ts +11 -0
- package/src/platform/Workspace.test.ts +4 -5
- package/src/platform/Workspace.ts +3 -3
- package/src/platform/WorktreeState.test.ts +2 -3
- package/src/platform/WorktreeState.ts +1 -1
- package/src/platform/index.ts +2 -1
- package/src/replay/Format.test.ts +35 -8
- package/src/replay/Format.ts +7 -4
- package/src/replay/Replay.test.ts +54 -0
- package/src/replay/Replay.ts +61 -17
- package/src/replay/Trailers.test.ts +12 -12
- package/src/replay/Trailers.ts +35 -20
- package/src/replay/Tree.ts +11 -0
- package/src/replay/index.ts +3 -0
- package/src/step/LandStep.ts +1 -0
- package/src/step/RepoSnapshot.ts +2 -0
- package/src/step/planStart.test.ts +2 -2
- package/src/step/planStart.ts +3 -3
- package/src/step/planStep.test.ts +4 -4
- package/src/step/planStep.ts +10 -8
- package/src/testkit/TestRepo.test.ts +4 -1
- package/src/testkit/TestRepo.ts +3 -1
- package/src/twins/EmittedScriptRecognizer.ts +32 -1
- package/src/twins/EmittedScripts.test.ts +1 -1
- package/src/twins/GitTiers.ts +29 -7
- package/src/twins/InMemRepo.ts +14 -0
- package/src/twins/Layers.test.ts +1 -1
- package/src/twins/Layers.ts +10 -3
- package/src/twins/PinBeats.ts +33 -0
- package/src/twins/cliIo.ts +9 -4
- package/src/twins/index.ts +1 -0
- package/src/wire/BeatDocument.ts +26 -5
- package/src/wire/Demand.ts +16 -6
- package/src/wire/constants.ts +3 -0
- package/src/wire/types.ts +4 -0
- package/src/workflow/discovery.test.ts +24 -7
- package/src/workflow/discovery.ts +5 -6
- package/src/workflow/index.ts +1 -0
- package/src/workflow/load.test.ts +60 -3
- package/src/workflow/load.ts +316 -166
- package/src/workflow/pin.test.ts +159 -0
- package/src/workflow/pin.ts +72 -0
- package/src/workflow/vars.ts +3 -3
- package/dist/chunks/testkit-BvecVjQ0.mjs +0 -32355
package/dist/testkit/gherkin.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { n as runsItsScript, r as testRepo } from "../chunks/testkit-
|
|
1
|
+
import { n as runsItsScript, r as testRepo } from "../chunks/testkit-C9sq0x07.mjs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import assert from "node:assert";
|
|
4
3
|
import { pathToFileURL } from "node:url";
|
|
5
4
|
import { After, AfterAll, Given, QuickPickleWorld, Then, When, setWorldConstructor } from "quickpickle";
|
|
5
|
+
import assert from "node:assert";
|
|
6
6
|
//#region src/gherkin/messages.ts
|
|
7
7
|
const CONTENT_LINES = 5;
|
|
8
8
|
const reason = (error) => error instanceof Error ? error.message : String(error);
|
package/dist/testkit.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { H as installContext } from "./chunks/flows-DENt0uWV.mjs";
|
|
2
|
-
import { a as verdictWhen, i as verdict, n as runsItsScript, o as whenPrompted, r as testRepo, t as inTurn } from "./chunks/testkit-
|
|
2
|
+
import { a as verdictWhen, i as verdict, n as runsItsScript, o as whenPrompted, r as testRepo, t as inTurn } from "./chunks/testkit-C9sq0x07.mjs";
|
|
3
3
|
export { inTurn, installContext, runsItsScript, testRepo, verdict, verdictWhen, whenPrompted };
|
package/dist/wire.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
export { ACCESS_REFUSAL, BEAT_KINDS, DEMAND_BRIEFING, EXIT_CODES, EXIT_OK, EXIT_RUNTIME_ERROR, EXIT_SIGINT, EXIT_SIGTERM, EXIT_STRAYS_FOUND, EXIT_USAGE_ERROR, UNATTRIBUTED_MODEL, WIRE_SCHEMA, beatDocument, beatKindOf, demandOf, globMatches, judgeJson, landFields, landProseText, noopText, renderBeatJson, renderBeatPlain, renderFormat, renderLandJson, renderLandPlain, renderScriptJson, resolveSession, scriptFields, selectPath, stallDiagnosis, statusOf };
|
|
1
|
+
import { A as UNATTRIBUTED_MODEL, C as EXIT_RUNTIME_ERROR, D as EXIT_USAGE_ERROR, E as EXIT_STRAYS_FOUND, M as renderFormat, O as ACCESS_REFUSAL, S as EXIT_OK, T as EXIT_SIGTERM, _ as BEAT_KINDS, a as renderScriptJson, b as stallDiagnosis, c as landProseText, d as renderLandPlain, f as DEMAND_BRIEFING, g as statusOf, h as renderBeatJson, i as judgeJson, j as WIRE_SCHEMA, k as PIN_PATH, l as noopText, m as beatDocument, n as resolveSession, o as scriptFields, p as renderBeatPlain, r as selectPath, s as landFields, t as globMatches, u as renderLandJson, v as beatKindOf, w as EXIT_SIGINT, x as EXIT_CODES, y as demandOf } from "./chunks/wire-CPNSm_9N.mjs";
|
|
2
|
+
export { ACCESS_REFUSAL, BEAT_KINDS, DEMAND_BRIEFING, EXIT_CODES, EXIT_OK, EXIT_RUNTIME_ERROR, EXIT_SIGINT, EXIT_SIGTERM, EXIT_STRAYS_FOUND, EXIT_USAGE_ERROR, PIN_PATH, UNATTRIBUTED_MODEL, WIRE_SCHEMA, beatDocument, beatKindOf, demandOf, globMatches, judgeJson, landFields, landProseText, noopText, renderBeatJson, renderBeatPlain, renderFormat, renderLandJson, renderLandPlain, renderScriptJson, resolveSession, scriptFields, selectPath, stallDiagnosis, statusOf };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmelab/gtd",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "61.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
|
|
6
6
|
"bin": {
|
|
@@ -76,14 +76,14 @@
|
|
|
76
76
|
"url": "https://github.com/pmelab/gtd.git"
|
|
77
77
|
},
|
|
78
78
|
"engines": {
|
|
79
|
-
"node": ">=22"
|
|
79
|
+
"node": ">=22.19.0"
|
|
80
80
|
},
|
|
81
81
|
"type": "module",
|
|
82
82
|
"packageManager": "npm@11.17.0",
|
|
83
83
|
"scripts": {
|
|
84
84
|
"prepare": "husky",
|
|
85
85
|
"dev": "node dev/run.mjs",
|
|
86
|
-
"build": "tsdown --filter web && npx @tailwindcss/cli -i src/web/styles.css -o dist/web/main.css --minify && node scripts/inline-web-client.mjs && tsdown --filter gtd && tsdown --filter entrypoints",
|
|
86
|
+
"build": "tsdown --filter web && npx @tailwindcss/cli -i src/web/styles.css -o dist/web/main.css --minify && node scripts/inline-web-client.mjs && tsdown --filter gtd --filter core && tsdown --filter entrypoints",
|
|
87
87
|
"postbuild": "node scripts/assert-no-test-doubles.mjs",
|
|
88
88
|
"watch": "tsdown --watch",
|
|
89
89
|
"bench:replay": "node scripts/bench-replay.mjs",
|
|
@@ -141,6 +141,7 @@
|
|
|
141
141
|
"fallow": "3.25.0",
|
|
142
142
|
"fast-check": "^4.8.0",
|
|
143
143
|
"husky": "^9.1.7",
|
|
144
|
+
"jiti": "^2.7.0",
|
|
144
145
|
"lint-staged": "^17.0.8",
|
|
145
146
|
"oxfmt": "^0.59.0",
|
|
146
147
|
"oxlint": "^1.74.0",
|
|
@@ -173,12 +174,10 @@
|
|
|
173
174
|
}
|
|
174
175
|
},
|
|
175
176
|
"dependencies": {
|
|
176
|
-
"@effect/platform": "^
|
|
177
|
-
"@effect/platform-node": "^0.104.1",
|
|
177
|
+
"@effect/platform-node-shared": "^4.1.0",
|
|
178
178
|
"@standard-schema/spec": "^1.1.0",
|
|
179
179
|
"@trpc/server": "^11.18.0",
|
|
180
|
-
"effect": "^
|
|
181
|
-
"jiti": "^2.7.0",
|
|
180
|
+
"effect": "^4.1.0",
|
|
182
181
|
"mdast-util-from-markdown": "^2.0.3",
|
|
183
182
|
"mdast-util-gfm-autolink-literal": "^2.0.1",
|
|
184
183
|
"mdast-util-gfm-footnote": "^2.1.0",
|
|
@@ -188,6 +187,7 @@
|
|
|
188
187
|
"micromark-extension-gfm-footnote": "^2.1.0",
|
|
189
188
|
"micromark-extension-gfm-strikethrough": "^2.1.0",
|
|
190
189
|
"micromark-extension-gfm-task-list-item": "^2.1.0",
|
|
190
|
+
"rolldown": "^1.1.5",
|
|
191
191
|
"vscode-languageserver": "^10.1.0",
|
|
192
192
|
"vscode-languageserver-textdocument": "^1.0.13"
|
|
193
193
|
}
|
package/src/cli/Cli.test.ts
CHANGED
|
@@ -1537,7 +1537,7 @@ describe("runCli — stdout stays byte-empty on every failing surface", () => {
|
|
|
1537
1537
|
repo.writeFile(".gtd/TODO.md", "## Open Questions\n\n###\n\nno question text.\n")
|
|
1538
1538
|
const stdoutCalls: string[] = []
|
|
1539
1539
|
const io: CliIo = {
|
|
1540
|
-
stdout: (chunk) => stdoutCalls.push(chunk),
|
|
1540
|
+
stdout: (chunk) => stdoutCalls.push(String(chunk)),
|
|
1541
1541
|
stderr: () => {},
|
|
1542
1542
|
exit: () => {},
|
|
1543
1543
|
layers: () => testLayers(repo),
|
|
@@ -1548,7 +1548,7 @@ describe("runCli — stdout stays byte-empty on every failing surface", () => {
|
|
|
1548
1548
|
})
|
|
1549
1549
|
|
|
1550
1550
|
describe("nodeCliIo", () => {
|
|
1551
|
-
const io = nodeCliIo(Layer.succeed(ToolCommands, {} as ToolCommands["
|
|
1551
|
+
const io = nodeCliIo(Layer.succeed(ToolCommands, {} as ToolCommands["Service"]))
|
|
1552
1552
|
|
|
1553
1553
|
it("exposes stdout/stderr/exit/layers", () => {
|
|
1554
1554
|
expect(typeof io.stdout).toBe("function")
|
package/src/cli/Cli.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Cause, Effect, Either, Layer } from "effect"
|
|
1
|
+
import { Cause, Effect, Layer, Result } from "effect"
|
|
3
2
|
import {
|
|
4
3
|
GtdError,
|
|
5
4
|
GtdUsageError,
|
|
@@ -9,6 +8,7 @@ import {
|
|
|
9
8
|
Host,
|
|
10
9
|
Workspace,
|
|
11
10
|
CommandRunner,
|
|
11
|
+
NodeServices,
|
|
12
12
|
} from "../platform/index.js"
|
|
13
13
|
import {
|
|
14
14
|
flagSettings,
|
|
@@ -20,6 +20,7 @@ import { GTD_VERSION } from "../install/index.js"
|
|
|
20
20
|
import {
|
|
21
21
|
ConfigDiscovery,
|
|
22
22
|
ConfigService,
|
|
23
|
+
EngineCore,
|
|
23
24
|
isSettingName,
|
|
24
25
|
SETTING_NAME_RULE,
|
|
25
26
|
StartFlags,
|
|
@@ -75,7 +76,7 @@ interface FlagRow {
|
|
|
75
76
|
readonly repeatable: boolean
|
|
76
77
|
readonly scope: FlagScope
|
|
77
78
|
/** All raw occurrences (already de-`=`-ed) → the decoded value, or an error message. */
|
|
78
|
-
readonly decode: (raws: readonly string[]) =>
|
|
79
|
+
readonly decode: (raws: readonly string[]) => Result.Result<unknown, string>
|
|
79
80
|
readonly scopeError: string
|
|
80
81
|
readonly valueHint: string
|
|
81
82
|
/** One sentence for a command's help, wrapped when rendered; `helpFor` overrides it for a command the flag means something else on. */
|
|
@@ -89,40 +90,40 @@ interface FlagRow {
|
|
|
89
90
|
const decodeAssignments = (
|
|
90
91
|
flag: string,
|
|
91
92
|
raws: readonly string[],
|
|
92
|
-
):
|
|
93
|
+
): Result.Result<Readonly<Record<string, string>>, string> => {
|
|
93
94
|
const vars: Record<string, string> = {}
|
|
94
95
|
for (const raw of raws) {
|
|
95
96
|
const eq = raw.indexOf("=")
|
|
96
97
|
if (eq <= 0) {
|
|
97
|
-
return
|
|
98
|
+
return Result.fail(`gtd: ${flag} must be <name>=<value> with a non-empty name — got "${raw}"`)
|
|
98
99
|
}
|
|
99
100
|
const name = raw.slice(0, eq)
|
|
100
101
|
const value = raw.slice(eq + 1)
|
|
101
102
|
if (!isSettingName(name)) {
|
|
102
|
-
return
|
|
103
|
+
return Result.fail(
|
|
103
104
|
`gtd: ${flag} ${JSON.stringify(name)} is not a valid setting name — ${SETTING_NAME_RULE}`,
|
|
104
105
|
)
|
|
105
106
|
}
|
|
106
|
-
if (/[\r\n]/.test(value)) return
|
|
107
|
+
if (/[\r\n]/.test(value)) return Result.fail(`gtd: ${flag} ${name} must be a single-line value`)
|
|
107
108
|
if (Object.hasOwn(vars, name))
|
|
108
|
-
return
|
|
109
|
+
return Result.fail(`gtd: ${flag} ${name} specified more than once`)
|
|
109
110
|
if (!name.startsWith("flag_")) {
|
|
110
111
|
vars[name] = value
|
|
111
112
|
continue
|
|
112
113
|
}
|
|
113
114
|
const pinned = flagSettingValue(value)
|
|
114
|
-
if (pinned === undefined) return
|
|
115
|
+
if (pinned === undefined) return Result.fail(`gtd: ${flag} ${name} must be 1, 0, true or false`)
|
|
115
116
|
vars[name] = pinned
|
|
116
117
|
}
|
|
117
|
-
return
|
|
118
|
+
return Result.succeed(vars)
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
const nonNegativeNumber = (raw: string, flag: string):
|
|
121
|
+
const nonNegativeNumber = (raw: string, flag: string): Result.Result<number, string> => {
|
|
121
122
|
const n = Number(raw)
|
|
122
123
|
if (raw.trim() === "" || !Number.isFinite(n) || n < 0) {
|
|
123
|
-
return
|
|
124
|
+
return Result.fail(`gtd: ${flag} must be a non-negative number — got "${raw}"`)
|
|
124
125
|
}
|
|
125
|
-
return
|
|
126
|
+
return Result.succeed(n)
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
/** The commands whose only output is a script: `--json` prints `{ schema, script }`. */
|
|
@@ -155,7 +156,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
155
156
|
arity: "optional",
|
|
156
157
|
repeatable: false,
|
|
157
158
|
scope: (kind) => JSON_KINDS.has(kind),
|
|
158
|
-
decode: ([raw]) =>
|
|
159
|
+
decode: ([raw]) => Result.succeed(raw ?? ""),
|
|
159
160
|
scopeError:
|
|
160
161
|
"gtd: --json is only valid for `gtd next`/`gtd land`, the commands that print a script, and the listings whose help names it (e.g. `gtd strays`) — " +
|
|
161
162
|
"every other command prints plain text; see `gtd guide` for the driver protocol briefing",
|
|
@@ -175,8 +176,8 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
175
176
|
decode: ([raw]) => {
|
|
176
177
|
const n = Number(raw)
|
|
177
178
|
return raw !== undefined && Number.isInteger(n) && n >= 0 && n <= 65535
|
|
178
|
-
?
|
|
179
|
-
:
|
|
179
|
+
? Result.succeed(n)
|
|
180
|
+
: Result.fail(`gtd: --port must be an integer 0–65535 (got '${raw ?? ""}')`)
|
|
180
181
|
},
|
|
181
182
|
scopeError: "gtd: --port is only valid for `gtd ui`",
|
|
182
183
|
valueHint: "<n>",
|
|
@@ -189,8 +190,8 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
189
190
|
scope: (kind) => kind === "ui",
|
|
190
191
|
decode: ([raw]) =>
|
|
191
192
|
raw === undefined || raw.trim() === "" || /[\r\n]/.test(raw)
|
|
192
|
-
?
|
|
193
|
-
:
|
|
193
|
+
? Result.fail("gtd: --host must be a non-empty, single-line value")
|
|
194
|
+
: Result.succeed(raw),
|
|
194
195
|
scopeError: "gtd: --host is only valid for `gtd ui`",
|
|
195
196
|
valueHint: "<addr>",
|
|
196
197
|
help: "Bind this address directly instead of publishing through tailscale serve",
|
|
@@ -200,7 +201,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
200
201
|
arity: 0,
|
|
201
202
|
repeatable: false,
|
|
202
203
|
scope: (kind) => kind === "ui",
|
|
203
|
-
decode: () =>
|
|
204
|
+
decode: () => Result.succeed(true),
|
|
204
205
|
scopeError: "gtd: --self-signed is only valid for `gtd ui`",
|
|
205
206
|
valueHint: "",
|
|
206
207
|
help: "Bind directly with a throwaway self-signed certificate instead of the configured ui.cert/ui.key",
|
|
@@ -210,7 +211,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
210
211
|
arity: 0,
|
|
211
212
|
repeatable: false,
|
|
212
213
|
scope: (kind) => kind === "ui",
|
|
213
|
-
decode: () =>
|
|
214
|
+
decode: () => Result.succeed(true),
|
|
214
215
|
scopeError: "gtd: --dev is only valid for `gtd ui`",
|
|
215
216
|
valueHint: "",
|
|
216
217
|
help: "Serve local development sources instead of the packaged build",
|
|
@@ -222,8 +223,8 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
222
223
|
scope: (kind) => kind === "judgeRun",
|
|
223
224
|
decode: ([raw]) =>
|
|
224
225
|
raw === "fixed" || raw === "jev" || raw === "llm"
|
|
225
|
-
?
|
|
226
|
-
:
|
|
226
|
+
? Result.succeed(raw)
|
|
227
|
+
: Result.fail(`gtd: --provider must be one of fixed, jev, llm — got "${raw ?? ""}"`),
|
|
227
228
|
scopeError: "gtd: --provider is only valid for `gtd judge run`",
|
|
228
229
|
valueHint: "<name>",
|
|
229
230
|
help: "The answerer: fixed, jev or llm (default: $GTD_JUDGE_PROVIDER, then ~/.config/gtd/judge.json, then jev when $TYPESAFE_API_KEY is set, else llm)",
|
|
@@ -235,8 +236,8 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
235
236
|
scope: (kind) => kind === "judgeRun",
|
|
236
237
|
decode: ([raw]) =>
|
|
237
238
|
raw === undefined || raw === ""
|
|
238
|
-
?
|
|
239
|
-
:
|
|
239
|
+
? Result.fail("gtd: --answers must be a non-empty path")
|
|
240
|
+
: Result.succeed(raw),
|
|
240
241
|
scopeError: "gtd: --answers is only valid for `gtd judge run`",
|
|
241
242
|
valueHint: "<path>",
|
|
242
243
|
help: "With --provider fixed: a JSON verdict file [{ id, answer, p }] (default: $GTD_JUDGE_ANSWERS, inline JSON)",
|
|
@@ -258,8 +259,8 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
258
259
|
scope: (kind) => kind === "land" || kind === "judgeRun",
|
|
259
260
|
decode: ([raw]) =>
|
|
260
261
|
raw === undefined || raw.trim() === "" || /[\r\n]/.test(raw)
|
|
261
|
-
?
|
|
262
|
-
:
|
|
262
|
+
? Result.fail("gtd: --model must be a non-empty, single-line value")
|
|
263
|
+
: Result.succeed(raw),
|
|
263
264
|
scopeError:
|
|
264
265
|
"gtd: --model is only valid for `gtd land` (with --cost: the model to record the cost against) or `gtd judge run` (the model that answers)",
|
|
265
266
|
valueHint: "<name>",
|
|
@@ -295,7 +296,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
295
296
|
arity: 0,
|
|
296
297
|
repeatable: false,
|
|
297
298
|
scope: (kind) => kind === "historyJudges",
|
|
298
|
-
decode: () =>
|
|
299
|
+
decode: () => Result.succeed(true),
|
|
299
300
|
scopeError: "gtd: --jsonl is only valid for `gtd history judges`",
|
|
300
301
|
valueHint: "",
|
|
301
302
|
help: "Print one JSON line per verdict instead of the report",
|
|
@@ -306,7 +307,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
306
307
|
arity: 0,
|
|
307
308
|
repeatable: false,
|
|
308
309
|
scope: (kind) => kind === "check",
|
|
309
|
-
decode: () =>
|
|
310
|
+
decode: () => Result.succeed(true),
|
|
310
311
|
scopeError: "gtd: --open-questions is only valid for `gtd check`",
|
|
311
312
|
valueHint: "",
|
|
312
313
|
help: "Instead of <mode>'s findings, list the unanswered questions in <file>; exits 1 when any remain",
|
|
@@ -316,7 +317,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
316
317
|
arity: 0,
|
|
317
318
|
repeatable: false,
|
|
318
319
|
scope: (kind) => kind === "check",
|
|
319
|
-
decode: () =>
|
|
320
|
+
decode: () => Result.succeed(true),
|
|
320
321
|
scopeError: "gtd: --open-threads is only valid for `gtd check`",
|
|
321
322
|
valueHint: "",
|
|
322
323
|
help: "Instead of <mode>'s findings, list the open threads in <file>; exits 1 when any are open. Alone, lists the open code-comment threads in the files the current process changed",
|
|
@@ -326,7 +327,7 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
326
327
|
arity: 0,
|
|
327
328
|
repeatable: false,
|
|
328
329
|
scope: () => true,
|
|
329
|
-
decode: () =>
|
|
330
|
+
decode: () => Result.succeed(true),
|
|
330
331
|
scopeError: "gtd: --verbose is valid for every command — this error is unreachable",
|
|
331
332
|
valueHint: "",
|
|
332
333
|
help: "Narrate on stderr what gtd resolved and where its config came from",
|
|
@@ -533,6 +534,14 @@ Prints a sorted JSON array ([] when clean) and a summary on stderr; --verbose ad
|
|
|
533
534
|
summary: "Print a briefing for writing a driver",
|
|
534
535
|
details: `Print a self-contained briefing that teaches an agent or a person to build a gtd driver in any shell or runtime, and where to get a workflow — gtd ships none. Writes nothing.`,
|
|
535
536
|
},
|
|
537
|
+
{
|
|
538
|
+
token: "bundle",
|
|
539
|
+
kind: "bundle",
|
|
540
|
+
arity: "none",
|
|
541
|
+
group: "Driver plumbing",
|
|
542
|
+
summary: "Print the pin of the current workflow",
|
|
543
|
+
details: `Print a pin to stdout: gtd's engine and the current workflow — gtd.config.ts, every module it imports and every npm package they use — as one gzipped ES module, after a plain-text header line \`gtdpin <format> gtd <version>\`. Git diffs it as binary. Refuses a workflow gtd would refuse, and a package that loads a native addon (.node), naming it. Writes nothing.`,
|
|
544
|
+
},
|
|
536
545
|
{
|
|
537
546
|
token: "judge",
|
|
538
547
|
kind: "judge",
|
|
@@ -916,7 +925,7 @@ const pairArityError = (
|
|
|
916
925
|
/** Decode every present flag into a bag of values, or the first decode error encountered. */
|
|
917
926
|
const decodeFlags = (
|
|
918
927
|
byFlag: ReadonlyMap<string, readonly string[]>,
|
|
919
|
-
):
|
|
928
|
+
): Result.Result<Readonly<Record<string, unknown>>, string> => {
|
|
920
929
|
const bag: Record<string, unknown> = {}
|
|
921
930
|
for (const row of FLAGS) {
|
|
922
931
|
const raws = byFlag.get(row.name)
|
|
@@ -925,19 +934,19 @@ const decodeFlags = (
|
|
|
925
934
|
// to decode — presence alone is tracked via `present`, not the bag.
|
|
926
935
|
if (row.arity === "optional" && raws.length === 0) continue
|
|
927
936
|
const decoded = row.decode(raws)
|
|
928
|
-
if (
|
|
929
|
-
bag[row.name] = decoded.
|
|
937
|
+
if (Result.isFailure(decoded)) return Result.fail(decoded.failure)
|
|
938
|
+
bag[row.name] = decoded.success
|
|
930
939
|
}
|
|
931
|
-
return
|
|
940
|
+
return Result.succeed(bag)
|
|
932
941
|
}
|
|
933
942
|
|
|
934
943
|
/** `gtd history judges`' thresholds: each a known judge threshold with a numeric value. */
|
|
935
944
|
const judgeThresholds = (
|
|
936
945
|
thresholds: Readonly<Record<string, string>>,
|
|
937
|
-
):
|
|
946
|
+
): Result.Result<Readonly<Record<string, string>>, string> => {
|
|
938
947
|
const unknown = Object.keys(thresholds).find((name) => !Object.hasOwn(THRESHOLDS, name))
|
|
939
948
|
if (unknown !== undefined) {
|
|
940
|
-
return
|
|
949
|
+
return Result.fail(
|
|
941
950
|
`gtd history judges: --threshold ${unknown} is no judge threshold — use ${Object.keys(THRESHOLDS).join(" or ")}`,
|
|
942
951
|
)
|
|
943
952
|
}
|
|
@@ -945,11 +954,11 @@ const judgeThresholds = (
|
|
|
945
954
|
([, value]) => value.trim() === "" || !Number.isFinite(Number(value)),
|
|
946
955
|
)
|
|
947
956
|
if (nan !== undefined) {
|
|
948
|
-
return
|
|
957
|
+
return Result.fail(
|
|
949
958
|
`gtd history judges: --threshold ${nan[0]} must be a number — got "${nan[1]}"`,
|
|
950
959
|
)
|
|
951
960
|
}
|
|
952
|
-
return
|
|
961
|
+
return Result.succeed(thresholds)
|
|
953
962
|
}
|
|
954
963
|
|
|
955
964
|
/** Assembles a `land` `Command` from its already-scope-checked flag bag — split out of `parseArgv` so its two independent, omit-when-absent optional fields don't inflate that function's own branching. */
|
|
@@ -1087,8 +1096,8 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1087
1096
|
if (conflict !== undefined) return usagePlan(conflict, jsonSeen)
|
|
1088
1097
|
|
|
1089
1098
|
const decoded = decodeFlags(byFlag)
|
|
1090
|
-
if (
|
|
1091
|
-
const bag = decoded.
|
|
1099
|
+
if (Result.isFailure(decoded)) return usagePlan(decoded.failure, jsonSeen)
|
|
1100
|
+
const bag = decoded.success as {
|
|
1092
1101
|
readonly "--json"?: string
|
|
1093
1102
|
readonly "--port"?: number
|
|
1094
1103
|
readonly "--cost"?: number
|
|
@@ -1231,8 +1240,8 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1231
1240
|
|
|
1232
1241
|
if (kind === "historyJudges") {
|
|
1233
1242
|
const thresholds = judgeThresholds(bag["--threshold"] ?? {})
|
|
1234
|
-
if (
|
|
1235
|
-
const vars = thresholds.
|
|
1243
|
+
if (Result.isFailure(thresholds)) return usagePlan(thresholds.failure, jsonSeen)
|
|
1244
|
+
const vars = thresholds.success
|
|
1236
1245
|
return {
|
|
1237
1246
|
kind: "command",
|
|
1238
1247
|
command: {
|
|
@@ -1295,6 +1304,7 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1295
1304
|
| "next"
|
|
1296
1305
|
| "validate"
|
|
1297
1306
|
| "guide"
|
|
1307
|
+
| "bundle"
|
|
1298
1308
|
| "base"
|
|
1299
1309
|
| "judge"
|
|
1300
1310
|
| "judgeAnswer"
|
|
@@ -1308,7 +1318,7 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1308
1318
|
// ---------------------------------------------------------------------------
|
|
1309
1319
|
|
|
1310
1320
|
export interface CliIo {
|
|
1311
|
-
readonly stdout: (chunk: string) => void
|
|
1321
|
+
readonly stdout: (chunk: string | Uint8Array) => void
|
|
1312
1322
|
readonly stderr: (chunk: string) => void
|
|
1313
1323
|
readonly exit: (code: number) => void
|
|
1314
1324
|
/**
|
|
@@ -1333,7 +1343,7 @@ const writeStderr = (chunk: string): void => {
|
|
|
1333
1343
|
export type LiveToolCommands = Layer.Layer<
|
|
1334
1344
|
ToolCommands,
|
|
1335
1345
|
never,
|
|
1336
|
-
Exclude<CommandRequirements, ToolCommands> |
|
|
1346
|
+
Exclude<CommandRequirements, ToolCommands> | NodeServices.NodeServices
|
|
1337
1347
|
>
|
|
1338
1348
|
|
|
1339
1349
|
export const nodeCliIo = (tools: LiveToolCommands): CliIo => ({
|
|
@@ -1358,20 +1368,21 @@ export const nodeCliIo = (tools: LiveToolCommands): CliIo => ({
|
|
|
1358
1368
|
// "command"`, so --version/--help/a usage error never provoke it.
|
|
1359
1369
|
layers: (verbose) =>
|
|
1360
1370
|
// Dependency order matters: Workspace.Live needs GitService, both need
|
|
1361
|
-
// Host, and GitService needs
|
|
1371
|
+
// Host, and GitService needs NodeServices' ChildProcessSpawner. Each
|
|
1362
1372
|
// `provideMerge` satisfies the layers above it AND stays in the output.
|
|
1363
1373
|
tools.pipe(
|
|
1364
1374
|
Layer.provideMerge(
|
|
1365
1375
|
Layer.mergeAll(
|
|
1366
1376
|
ConfigService.Live,
|
|
1367
1377
|
ConfigDiscovery.Live,
|
|
1378
|
+
EngineCore.Live,
|
|
1368
1379
|
Workspace.Live,
|
|
1369
1380
|
Narrator.layer(writeStderr, verbose),
|
|
1370
1381
|
CommandRunner.Live,
|
|
1371
1382
|
).pipe(
|
|
1372
1383
|
Layer.provideMerge(GitService.Live),
|
|
1373
1384
|
Layer.provideMerge(Host.Live),
|
|
1374
|
-
Layer.provideMerge(
|
|
1385
|
+
Layer.provideMerge(NodeServices.layer),
|
|
1375
1386
|
),
|
|
1376
1387
|
),
|
|
1377
1388
|
),
|
|
@@ -1386,14 +1397,20 @@ export const nodeCliIo = (tools: LiveToolCommands): CliIo => ({
|
|
|
1386
1397
|
export const normalizeTrailingNewline = (artifact: string): string =>
|
|
1387
1398
|
artifact.length === 0 ? artifact : `${artifact.replace(/\n+$/, "")}\n`
|
|
1388
1399
|
|
|
1400
|
+
// Bytes are a binary artifact, written as they are.
|
|
1389
1401
|
const bufferedArtifactOut = (io: CliIo): ArtifactOut => {
|
|
1390
|
-
let buffer = ""
|
|
1402
|
+
let buffer: string | Uint8Array = ""
|
|
1391
1403
|
return {
|
|
1392
1404
|
write: (chunk) => {
|
|
1393
|
-
buffer
|
|
1405
|
+
buffer =
|
|
1406
|
+
typeof buffer === "string" && typeof chunk === "string"
|
|
1407
|
+
? buffer + chunk
|
|
1408
|
+
: Buffer.concat([Buffer.from(buffer), Buffer.from(chunk)])
|
|
1394
1409
|
},
|
|
1395
1410
|
flush: () => {
|
|
1396
|
-
if (buffer.length > 0)
|
|
1411
|
+
if (buffer.length > 0) {
|
|
1412
|
+
io.stdout(typeof buffer === "string" ? normalizeTrailingNewline(buffer) : buffer)
|
|
1413
|
+
}
|
|
1397
1414
|
buffer = ""
|
|
1398
1415
|
},
|
|
1399
1416
|
}
|
|
@@ -1479,8 +1496,8 @@ export const runCli = (argv: readonly string[], io: CliIo): Effect.Effect<void,
|
|
|
1479
1496
|
// (Ctrl-C on `ui`/`lsp`'s blocking `Effect.never`) is passed
|
|
1480
1497
|
// through untouched via `failCause` so `NodeRuntime.runMain` still sees a
|
|
1481
1498
|
// real interruption; everything else gets the envelope.
|
|
1482
|
-
Effect.
|
|
1483
|
-
Cause.
|
|
1499
|
+
Effect.catch((cause) =>
|
|
1500
|
+
Cause.hasInterruptsOnly(cause)
|
|
1484
1501
|
? Effect.failCause(cause)
|
|
1485
1502
|
: report(io, plan.json.kind !== "off")(cause),
|
|
1486
1503
|
),
|
package/src/cli/History.test.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { Effect, Exit } from "effect"
|
|
2
2
|
import { describe, expect, it } from "vitest"
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
applyEmittedScript,
|
|
5
|
+
InMemRepo,
|
|
6
|
+
makeCapturingCliIo,
|
|
7
|
+
takePinBeat,
|
|
8
|
+
testLayers,
|
|
9
|
+
} from "../twins/index.js"
|
|
4
10
|
import { runCli } from "./index.js"
|
|
5
11
|
import { historyEntries, resolveHistoryRef, type HistoryEntry } from "./History.js"
|
|
6
12
|
|
|
@@ -12,7 +18,9 @@ export default async () => {
|
|
|
12
18
|
}
|
|
13
19
|
`
|
|
14
20
|
|
|
21
|
+
/** Lands `files` as one turn, taking a pin beat before it and an unpin beat after it as a driver does. */
|
|
15
22
|
const land = async (repo: InMemRepo, files: Record<string, string>): Promise<string> => {
|
|
23
|
+
await takePinBeat(repo, "add")
|
|
16
24
|
for (const [path, content] of Object.entries(files)) repo.writeFile(path, content)
|
|
17
25
|
const { io, result } = makeCapturingCliIo(repo)
|
|
18
26
|
await Effect.runPromise(runCli(["node", "gtd.js", "land", "--json"], io))
|
|
@@ -24,6 +32,7 @@ const land = async (repo: InMemRepo, files: Record<string, string>): Promise<str
|
|
|
24
32
|
(JSON.parse(stdout) as { script: string }).script,
|
|
25
33
|
)
|
|
26
34
|
if (!outcome.ok) throw new Error(outcome.error)
|
|
35
|
+
await takePinBeat(repo, "remove")
|
|
27
36
|
return repo.resolveRef("HEAD")!
|
|
28
37
|
}
|
|
29
38
|
|
package/src/cli/Metrics.test.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
2
|
import { describe, expect, it } from "vitest"
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
applyEmittedScript,
|
|
5
|
+
InMemRepo,
|
|
6
|
+
makeCapturingCliIo,
|
|
7
|
+
takePinBeat,
|
|
8
|
+
testLayers,
|
|
9
|
+
} from "../twins/index.js"
|
|
4
10
|
import { runCli } from "./index.js"
|
|
5
11
|
import { processMetrics, readProcessMetrics, totalMetrics, type MetricCommit } from "./Metrics.js"
|
|
6
12
|
|
|
@@ -213,7 +219,9 @@ export default async () => {
|
|
|
213
219
|
}
|
|
214
220
|
`
|
|
215
221
|
|
|
222
|
+
/** Lands `files` as one turn, taking a pin beat before it and an unpin beat after it as a driver does. */
|
|
216
223
|
const land = async (repo: InMemRepo, files: Record<string, string>): Promise<string> => {
|
|
224
|
+
await takePinBeat(repo, "add")
|
|
217
225
|
for (const [path, content] of Object.entries(files)) repo.writeFile(path, content)
|
|
218
226
|
const { io, result } = makeCapturingCliIo(repo)
|
|
219
227
|
await Effect.runPromise(runCli(["node", "gtd.js", "land", "--json"], io))
|
|
@@ -225,6 +233,7 @@ const land = async (repo: InMemRepo, files: Record<string, string>): Promise<str
|
|
|
225
233
|
(JSON.parse(stdout) as { script: string }).script,
|
|
226
234
|
)
|
|
227
235
|
if (!outcome.ok) throw new Error(outcome.error)
|
|
236
|
+
await takePinBeat(repo, "remove")
|
|
228
237
|
return repo.resolveRef("HEAD")!
|
|
229
238
|
}
|
|
230
239
|
|
package/src/cli/Output.test.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { writeScript, type JsonMode } from "./Output.js"
|
|
|
5
5
|
|
|
6
6
|
const run = (json: JsonMode, script: string, plain?: string) => {
|
|
7
7
|
let written = ""
|
|
8
|
-
const out = { write: (chunk: string) => (written += chunk), flush: () => {} }
|
|
8
|
+
const out = { write: (chunk: string | Uint8Array) => (written += String(chunk)), flush: () => {} }
|
|
9
9
|
const exit = Effect.runSyncExit(writeScript(json, out, script, plain))
|
|
10
10
|
return { exit, written }
|
|
11
11
|
}
|
package/src/cli/Output.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type EXIT_OK,
|
|
8
8
|
type EXIT_STRAYS_FOUND,
|
|
9
9
|
} from "../wire/index.js"
|
|
10
|
-
import { ConfigDiscovery, ConfigService } from "../workflow/index.js"
|
|
10
|
+
import { ConfigDiscovery, ConfigService, EngineCore } from "../workflow/index.js"
|
|
11
11
|
import type { ToolCommands } from "./ToolCommands.js"
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -59,7 +59,7 @@ export type JsonMode =
|
|
|
59
59
|
* reach stdout.
|
|
60
60
|
*/
|
|
61
61
|
export interface ArtifactOut {
|
|
62
|
-
readonly write: (chunk: string) => void
|
|
62
|
+
readonly write: (chunk: string | Uint8Array) => void
|
|
63
63
|
readonly flush: () => void
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -67,6 +67,7 @@ export type CommandRequirements =
|
|
|
67
67
|
| GitService
|
|
68
68
|
| ConfigService
|
|
69
69
|
| ConfigDiscovery
|
|
70
|
+
| EngineCore
|
|
70
71
|
| Workspace
|
|
71
72
|
| Host
|
|
72
73
|
| Narrator
|