@pmelab/gtd 12.5.0 → 14.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/README.md +49 -38
- package/dist/gtd.bundle.mjs +3572 -11508
- package/package.json +8 -3
- package/schema.json +5 -287
- package/src/flows/helpers.ts +93 -0
- package/src/flows/index.ts +3 -0
- package/src/flows/runtime.ts +295 -0
- package/src/flows/scripts.ts +116 -0
- package/src/workflows/health.ts +126 -0
- package/src/workflows/index.ts +1 -0
- package/src/workflows/packages.ts +93 -0
- package/src/workflows/planning.ts +99 -0
- package/src/workflows/prose.ts +200 -0
- package/src/workflows/review.ts +169 -0
- package/src/workflows/steps.ts +200 -0
- package/src/workflows/text.fixture.ts +38 -0
- package/src/workflows/text.test.ts +37 -0
- package/src/workflows/text.ts +682 -0
- package/src/workflows/unified.ts +128 -0
- package/src/workflows/vars.ts +23 -0
package/package.json
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmelab/gtd",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "14.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": {
|
|
7
7
|
"gtd": "dist/gtd.bundle.mjs"
|
|
8
8
|
},
|
|
9
|
+
"exports": {
|
|
10
|
+
"./flows": "./src/flows/index.ts",
|
|
11
|
+
"./workflow": "./src/workflows/unified.ts"
|
|
12
|
+
},
|
|
9
13
|
"files": [
|
|
10
14
|
"dist/",
|
|
15
|
+
"src/flows/",
|
|
16
|
+
"src/workflows/",
|
|
11
17
|
"README.md",
|
|
12
18
|
"LICENSE",
|
|
13
19
|
"schema.json"
|
|
@@ -85,7 +91,6 @@
|
|
|
85
91
|
"fallow": "3.25.0",
|
|
86
92
|
"fast-check": "^4.8.0",
|
|
87
93
|
"husky": "^9.1.7",
|
|
88
|
-
"jiti": "^2.7.0",
|
|
89
94
|
"lint-staged": "^17.0.8",
|
|
90
95
|
"oxfmt": "^0.59.0",
|
|
91
96
|
"oxlint": "^1.74.0",
|
|
@@ -112,7 +117,7 @@
|
|
|
112
117
|
"@trpc/server": "^11.18.0",
|
|
113
118
|
"cosmiconfig": "^9.0.2",
|
|
114
119
|
"effect": "^3.19.15",
|
|
115
|
-
"
|
|
120
|
+
"jiti": "^2.7.0",
|
|
116
121
|
"mdast-util-from-markdown": "^2.0.3",
|
|
117
122
|
"mdast-util-gfm-footnote": "^2.1.0",
|
|
118
123
|
"mdast-util-gfm-task-list-item": "^2.0.0",
|
package/schema.json
CHANGED
|
@@ -3,291 +3,9 @@
|
|
|
3
3
|
"type": "object",
|
|
4
4
|
"required": [],
|
|
5
5
|
"properties": {
|
|
6
|
-
"workflow": {
|
|
7
|
-
"type": "object",
|
|
8
|
-
"description": "The whole machine definition: a tree of named machines rooted at entry.default (plus the workflow's own vars: defaults, modes: steering-file modes, and summary: template). Compiled and validated by gtd at load time; content strings starting with ./ or ../ are file references inlined from the config file's directory (a modes: command never is — it is a shell command).",
|
|
9
|
-
"additionalProperties": false,
|
|
10
|
-
"required": [
|
|
11
|
-
"entry",
|
|
12
|
-
"machines"
|
|
13
|
-
],
|
|
14
|
-
"properties": {
|
|
15
|
-
"vars": {
|
|
16
|
-
"type": "object",
|
|
17
|
-
"description": "Flat name -> scalar map merged into every template's it.vars. Scalars are coerced to strings.",
|
|
18
|
-
"additionalProperties": {
|
|
19
|
-
"type": [
|
|
20
|
-
"string",
|
|
21
|
-
"number",
|
|
22
|
-
"boolean"
|
|
23
|
-
]
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
"modes": {
|
|
27
|
-
"type": "object",
|
|
28
|
-
"description": "Steering-file modes a state's mode: may name. Each entry declares at least one of format/validate: shell commands (Eta templates seeing it.file = the rendered steering-file path) gtd runs via bash. format rewrites the file in place; validate exits 0 when valid, non-zero with findings on stdout/stderr otherwise. The halves layer independently, so naming a built-in mode (qa/review) and declaring only format: adds formatting while keeping gtd's own validation. gtd ships no formatter — bring your own (prettier, dprint, a script).",
|
|
29
|
-
"additionalProperties": {
|
|
30
|
-
"type": "object",
|
|
31
|
-
"description": "One mode: at least one of format/validate.",
|
|
32
|
-
"additionalProperties": false,
|
|
33
|
-
"minProperties": 1,
|
|
34
|
-
"properties": {
|
|
35
|
-
"format": {
|
|
36
|
-
"type": "string",
|
|
37
|
-
"description": "Shell command that rewrites the steering file in place before validation (Eta template; it.file is the file path). A non-zero exit is a hard error."
|
|
38
|
-
},
|
|
39
|
-
"validate": {
|
|
40
|
-
"type": "string",
|
|
41
|
-
"description": "Shell command that validates the steering file (Eta template; it.file is the file path). Exit 0 = valid; non-zero = invalid, with its output reported as the findings."
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
"summary": {
|
|
47
|
-
"type": "string",
|
|
48
|
-
"description": "Eta template rendered by `gtd summary`: instructions plus commit hashes (it.entryCommit, it.humanCommits, it.processBase, it.processTip) for an agent to write the process's own closing message. A ./ or ../ value is inlined from the config file's directory at load time. Absent is legal (gtd summary refuses); present-but-blank is a load error."
|
|
49
|
-
},
|
|
50
|
-
"entry": {
|
|
51
|
-
"type": "object",
|
|
52
|
-
"description": "The workflow's root machine, resolved through the same resolver an `on`/`retry.otherwise` target uses (see src/Machines.ts) — accepts either a bare state path or an instance/reference-key path. Extra manual entry points are declared per-state instead, via a state's own `entry: true` (see `stateJsonSchema`'s `entry` property) — not here.",
|
|
53
|
-
"additionalProperties": false,
|
|
54
|
-
"required": [
|
|
55
|
-
"default"
|
|
56
|
-
],
|
|
57
|
-
"properties": {
|
|
58
|
-
"default": {
|
|
59
|
-
"type": "string",
|
|
60
|
-
"description": "Which declared `machines:` entry is the ROOT instance."
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
"machines": {
|
|
65
|
-
"type": "object",
|
|
66
|
-
"description": "Named, reusable machines — at least the one entry.default names.",
|
|
67
|
-
"minProperties": 1,
|
|
68
|
-
"additionalProperties": {
|
|
69
|
-
"type": "object",
|
|
70
|
-
"description": "A named, reusable machine: an entry local plus a set of states, each either an ordinary state or a reference instantiating another machine as a child (see src/Machines.ts).",
|
|
71
|
-
"additionalProperties": false,
|
|
72
|
-
"required": [
|
|
73
|
-
"entry",
|
|
74
|
-
"states"
|
|
75
|
-
],
|
|
76
|
-
"properties": {
|
|
77
|
-
"params": {
|
|
78
|
-
"type": "array",
|
|
79
|
-
"items": {
|
|
80
|
-
"type": "string"
|
|
81
|
-
},
|
|
82
|
-
"description": "Advisory only — documents which $params a caller may bind via a reference's `with:`. A `$name` token used as a whole field value or `on`/`retry.otherwise` target is resolved against the binding."
|
|
83
|
-
},
|
|
84
|
-
"entry": {
|
|
85
|
-
"type": "string",
|
|
86
|
-
"description": "This machine's own default local (a local state name, or a reference key), resolved recursively."
|
|
87
|
-
},
|
|
88
|
-
"model": {
|
|
89
|
-
"type": "string",
|
|
90
|
-
"description": "Opaque harness hint stamped onto every one of this machine's own `prompt` states (e.g. \"smart\"), passed through `gtd next --json`/`gtd status --json`. The ONLY place a model may be declared — a state carrying its own `model:` is a config error. Never interpreted by gtd. A machine declaring this with no `prompt` state is a config error."
|
|
91
|
-
},
|
|
92
|
-
"system": {
|
|
93
|
-
"type": "string",
|
|
94
|
-
"description": "The agent harness system prompt stamped onto every one of this machine's own `prompt` states — emitted verbatim in `gtd next --json` for the driver to pass to its agent CLI, never interpreted by gtd. Machine-level only: a machine's `prompt` states share one resumed session, and a system prompt must be identical across every call of it. A `./` or `../` value is inlined from the declaring config file's directory. A machine declaring this with no `prompt` state is a config error."
|
|
95
|
-
},
|
|
96
|
-
"states": {
|
|
97
|
-
"type": "object",
|
|
98
|
-
"description": "This machine's local states, each either an ordinary state or a `{ machine, with }` reference.",
|
|
99
|
-
"minProperties": 1,
|
|
100
|
-
"additionalProperties": {
|
|
101
|
-
"oneOf": [
|
|
102
|
-
{
|
|
103
|
-
"type": "object",
|
|
104
|
-
"description": "One workflow state. Declare exactly one content kind (script/prompt/message).",
|
|
105
|
-
"additionalProperties": false,
|
|
106
|
-
"properties": {
|
|
107
|
-
"actor": {
|
|
108
|
-
"type": "string",
|
|
109
|
-
"description": "Who acts at this state. Required on every state."
|
|
110
|
-
},
|
|
111
|
-
"script": {
|
|
112
|
-
"type": "string",
|
|
113
|
-
"description": "Content kind: a shell script (Eta template). The loop driver executes it verbatim via bash, then steps this state's actor."
|
|
114
|
-
},
|
|
115
|
-
"prompt": {
|
|
116
|
-
"type": "string",
|
|
117
|
-
"description": "Content kind: an agent prompt (Eta template), emitted by `gtd next`."
|
|
118
|
-
},
|
|
119
|
-
"message": {
|
|
120
|
-
"type": "string",
|
|
121
|
-
"description": "Content kind: a human-facing message (Eta template), emitted by `gtd next`."
|
|
122
|
-
},
|
|
123
|
-
"on": {
|
|
124
|
-
"type": "object",
|
|
125
|
-
"description": "Ordered map of change pattern -> target state. Patterns: \"C\" (clean tree) or \"<A|M|D|*> <glob>\" over the pending diff; first declared match wins. Every target must name a defined state, and every non-initial state must be reachable through these edges (or a retry.otherwise). A value is either the target state name (string) or a { to, describe, action } object whose describe/action are human-readable strings templates can surface as it.edges (e.g. in a human gate's message).",
|
|
126
|
-
"additionalProperties": {
|
|
127
|
-
"oneOf": [
|
|
128
|
-
{
|
|
129
|
-
"type": "string",
|
|
130
|
-
"description": "The target state name."
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
"type": "object",
|
|
134
|
-
"additionalProperties": false,
|
|
135
|
-
"required": [
|
|
136
|
-
"to"
|
|
137
|
-
],
|
|
138
|
-
"properties": {
|
|
139
|
-
"to": {
|
|
140
|
-
"type": "string",
|
|
141
|
-
"description": "The target state name."
|
|
142
|
-
},
|
|
143
|
-
"describe": {
|
|
144
|
-
"type": "string",
|
|
145
|
-
"description": "Human-readable sentence describing where this change routes; surfaced verbatim (never Eta-rendered) to templates as it.edges[].describe."
|
|
146
|
-
},
|
|
147
|
-
"action": {
|
|
148
|
-
"type": "string",
|
|
149
|
-
"description": "Imperative label for this edge (e.g. \"Accept plan\"); surfaced verbatim (never Eta-rendered) to templates/tooling as it.edges[].action."
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
]
|
|
154
|
-
}
|
|
155
|
-
},
|
|
156
|
-
"retry": {
|
|
157
|
-
"type": "object",
|
|
158
|
-
"description": "Redirect transitions INTO this state once it has been entered `max` times in the current process.",
|
|
159
|
-
"additionalProperties": false,
|
|
160
|
-
"required": [
|
|
161
|
-
"max",
|
|
162
|
-
"otherwise"
|
|
163
|
-
],
|
|
164
|
-
"properties": {
|
|
165
|
-
"max": {
|
|
166
|
-
"type": "integer",
|
|
167
|
-
"minimum": 0,
|
|
168
|
-
"description": "Entries allowed this process before redirecting."
|
|
169
|
-
},
|
|
170
|
-
"otherwise": {
|
|
171
|
-
"type": "string",
|
|
172
|
-
"description": "Defined state to redirect to once over the cap."
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
},
|
|
176
|
-
"judge": {
|
|
177
|
-
"type": "string",
|
|
178
|
-
"description": "Eta template rendered ALONGSIDE this state's message: (content kind stays message — no sixth content kind). Must render to the JSON document { state, questions: [{ id, primitive, instructions, criteria }] }: `state` is evidence the template gathers only through it.read(...)/git helpers a prompt template already uses — never an uncommitted, freshly-gathered artifact. `questions` is part of the RENDERED document, not a separate structured YAML field — write it as ordinary JSON text inside this same template (a literal array, or computed however you like); gtd never parses it as YAML. `primitive` is one of noul (yes/no), choice, score. An unaware driver ignores this field and shows message: to a human; an aware driver pipes it to a judge model and answers with `gtd judge answer`. Requires a sibling `message:`."
|
|
179
|
-
},
|
|
180
|
-
"routes": {
|
|
181
|
-
"type": "array",
|
|
182
|
-
"description": "Ordered list of judgment routing rows — a probability is not an \"on\" change pattern, so this is its own engine routing language. Each row is { question, is, minP, maxP, to }: question names one of this state's own judge: question ids, is the expected answer, minP the probability floor (>=) and maxP the probability ceiling (<) that answer's own p must clear (both Eta templates, typically a workflow var:; either alone is legal). First declared match wins, exactly like \"on\". The list MUST end with a catch-all row carrying only \"to\" — a list without one is a load error. A conjunction (\"all of N questions answered yes at p >= threshold\") is expressed by inversion: per question, one escape row for the wrong answer plus one for the right answer under maxP=threshold, routing to the conservative target, followed by the catch-all routing to the optimistic one. Requires a sibling `judge:`.",
|
|
183
|
-
"items": {
|
|
184
|
-
"type": "object",
|
|
185
|
-
"additionalProperties": false,
|
|
186
|
-
"required": [
|
|
187
|
-
"to"
|
|
188
|
-
],
|
|
189
|
-
"properties": {
|
|
190
|
-
"question": {
|
|
191
|
-
"type": "string",
|
|
192
|
-
"description": "One of this state's own judge: question ids."
|
|
193
|
-
},
|
|
194
|
-
"is": {
|
|
195
|
-
"type": "string",
|
|
196
|
-
"description": "The expected answer for this row to match."
|
|
197
|
-
},
|
|
198
|
-
"minP": {
|
|
199
|
-
"type": "string",
|
|
200
|
-
"description": "Probability floor (>=) the answer's own p must clear (an Eta template, typically a workflow var: reference)."
|
|
201
|
-
},
|
|
202
|
-
"maxP": {
|
|
203
|
-
"type": "string",
|
|
204
|
-
"description": "Probability ceiling (<) the answer's own p must stay under (an Eta template, typically a workflow var: reference)."
|
|
205
|
-
},
|
|
206
|
-
"to": {
|
|
207
|
-
"type": "string",
|
|
208
|
-
"description": "The target state name."
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
},
|
|
213
|
-
"shadow": {
|
|
214
|
-
"type": "boolean",
|
|
215
|
-
"description": "When true, this state's judge: verdict is recorded (Gtd-Judge: trailer) but never consulted for routing — the state routes exactly as if judge: were absent. A debugging switch a repo turns on to diagnose a live threshold, not a release stage every gate passes through. Requires a sibling `judge:`."
|
|
216
|
-
},
|
|
217
|
-
"label": {
|
|
218
|
-
"type": "string",
|
|
219
|
-
"description": "Opaque display name passed through `gtd next --json`/`gtd status --json` so a driver/viewer can show something nicer than the raw state name (e.g. \"Running checks\"). Never interpreted by gtd."
|
|
220
|
-
},
|
|
221
|
-
"skills": {
|
|
222
|
-
"type": "string",
|
|
223
|
-
"description": "The skills this state's agent should load, as prose for its own harness — an Eta template (typically a workflow var: reference). A literal `skills: \"\"` is rejected at load (nonEmpty); a value that RENDERS blank is legal and means \"no skills at this state this run\" — the preamble is simply omitted. gtd never resolves, loads, or validates a skill name; it is concatenated into the prompt untouched, prepended via the `skillsPreamble` var (blank that var to switch the mechanism off repo-wide). Requires a sibling `prompt:`."
|
|
224
|
-
},
|
|
225
|
-
"file": {
|
|
226
|
-
"type": "string",
|
|
227
|
-
"description": "The state's steering file: an Eta template naming the file a human/editor should look at while the machine rests here, RELATIVE to \".gtd/\" — the compiler prepends that directory automatically. A \"..\" segment, an absolute path, or an already-prefixed \".gtd/\" are rejected, not rewritten."
|
|
228
|
-
},
|
|
229
|
-
"mode": {
|
|
230
|
-
"type": "string",
|
|
231
|
-
"description": "The steering file's format: the name of a built-in mode (qa/review, validated in-process by gtd) or of a `modes:` entry. gtd formats and validates the file with that mode before capturing a turn out of this state, and the LSP dispatches live diagnostics on the built-in names. Requires a sibling `file:`."
|
|
232
|
-
},
|
|
233
|
-
"reviewBase": {
|
|
234
|
-
"oneOf": [
|
|
235
|
-
{
|
|
236
|
-
"const": true
|
|
237
|
-
},
|
|
238
|
-
{
|
|
239
|
-
"type": "string"
|
|
240
|
-
}
|
|
241
|
-
],
|
|
242
|
-
"description": "true marks the state whose most-recent in-process commit anchors the review window's diff base; absent any, the base is the process start. A string is a different shape: an Eta template rendering a commitish that becomes the WHOLE PROCESS's fixed diff base when this state is entered manually via `gtd --entry <state> --base <commitish>` (see the `entry` property below)."
|
|
243
|
-
},
|
|
244
|
-
"requireProgress": {
|
|
245
|
-
"type": "boolean",
|
|
246
|
-
"description": "When true, a step at this state is refused if its only pending change is deleting the state's own `file:` — a work-free turn that discards its input without addressing it. A `NOTHING ACTIONABLE` sentinel file is exempt (a legitimately non-actionable round makes no code change). Requires a `file:`."
|
|
247
|
-
},
|
|
248
|
-
"answerGate": {
|
|
249
|
-
"type": "boolean",
|
|
250
|
-
"description": "When true, a step at this state is refused unless every open question in its qa-mode `file:` is answered — exactly one checkbox ticked per question — except a wholly untouched working tree, which passes the gate; any other change, code included, still refuses on unanswered questions. Requires a `file:` and `mode: qa`."
|
|
251
|
-
},
|
|
252
|
-
"requireRevert": {
|
|
253
|
-
"type": "boolean",
|
|
254
|
-
"description": "When true, a step at this state is refused unless the paths the human's own review-round commit touched have actually been reverted out of the working tree — re-established from the tree itself, never from the script's own exit code. Requires a `file:`."
|
|
255
|
-
},
|
|
256
|
-
"entry": {
|
|
257
|
-
"const": true,
|
|
258
|
-
"description": "Marks this state as an extra manual entry point (WorkflowEntries.manual), enterable via `gtd --entry <state>`. Not to be confused with the top-level `entry:` key naming the root machine (entry.default) — same name, different level, by design."
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
"type": "object",
|
|
264
|
-
"description": "A reference: instantiates the named machine as a child, at this local's path (see src/Machines.ts). Expanded at load time into concrete, qualified states — the engine only ever sees the flattened result.",
|
|
265
|
-
"additionalProperties": false,
|
|
266
|
-
"required": [
|
|
267
|
-
"machine"
|
|
268
|
-
],
|
|
269
|
-
"properties": {
|
|
270
|
-
"machine": {
|
|
271
|
-
"type": "string",
|
|
272
|
-
"description": "Name of a declared entry in `machines:`."
|
|
273
|
-
},
|
|
274
|
-
"with": {
|
|
275
|
-
"type": "object",
|
|
276
|
-
"description": "Bindings for the referenced machine's `params:`. A bound value naming another of the CALLER's own bindings (a whole-value `$name`) passes it down verbatim, scope intact."
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
]
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
},
|
|
288
6
|
"vars": {
|
|
289
7
|
"type": "object",
|
|
290
|
-
"description": "Flat name -> scalar map merged into
|
|
8
|
+
"description": "Flat name -> scalar map merged into the workflow's vars. Scalars are coerced to strings.",
|
|
291
9
|
"additionalProperties": {
|
|
292
10
|
"type": [
|
|
293
11
|
"string",
|
|
@@ -298,7 +16,7 @@
|
|
|
298
16
|
},
|
|
299
17
|
"modes": {
|
|
300
18
|
"type": "object",
|
|
301
|
-
"description": "Steering-file modes a
|
|
19
|
+
"description": "Steering-file modes a workflow step's mode: may name. Each entry declares at least one of format/validate: shell commands gtd runs via bash with $GTD_FILE set to the steering file's path. format rewrites the file in place; validate exits 0 when valid, non-zero with findings on stdout/stderr otherwise. The halves layer independently, so naming a built-in mode (qa/review) and declaring only format: adds formatting while keeping gtd's own validation. gtd ships no formatter — bring your own (prettier, dprint, a script).",
|
|
302
20
|
"additionalProperties": {
|
|
303
21
|
"type": "object",
|
|
304
22
|
"description": "One mode: at least one of format/validate.",
|
|
@@ -307,11 +25,11 @@
|
|
|
307
25
|
"properties": {
|
|
308
26
|
"format": {
|
|
309
27
|
"type": "string",
|
|
310
|
-
"description": "Shell command that rewrites the steering file in place before validation (
|
|
28
|
+
"description": "Shell command that rewrites the steering file in place before validation ($GTD_FILE is the file path). A non-zero exit is a hard error."
|
|
311
29
|
},
|
|
312
30
|
"validate": {
|
|
313
31
|
"type": "string",
|
|
314
|
-
"description": "Shell command that validates the steering file (
|
|
32
|
+
"description": "Shell command that validates the steering file ($GTD_FILE is the file path). Exit 0 = valid; non-zero = invalid, with its output reported as the findings."
|
|
315
33
|
}
|
|
316
34
|
}
|
|
317
35
|
}
|
|
@@ -339,7 +57,7 @@
|
|
|
339
57
|
},
|
|
340
58
|
"format": {
|
|
341
59
|
"type": "string",
|
|
342
|
-
"description": "Shell command run after every UI write, before it resolves (
|
|
60
|
+
"description": "Shell command run after every UI write, before it resolves ($GTD_FILE is the written file's absolute path). A non-zero exit or missing binary never reverts the write or refuses it — it's reported to the client as a notice naming the command and its exit code. Absent means no command runs at all."
|
|
343
61
|
}
|
|
344
62
|
}
|
|
345
63
|
},
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {
|
|
2
|
+
changes,
|
|
3
|
+
head,
|
|
4
|
+
openQuestions,
|
|
5
|
+
read,
|
|
6
|
+
refuse,
|
|
7
|
+
run,
|
|
8
|
+
type Change,
|
|
9
|
+
type JudgeAnswer,
|
|
10
|
+
} from "./runtime.js"
|
|
11
|
+
import { checkScript } from "./scripts.js"
|
|
12
|
+
|
|
13
|
+
/** Whether a judge answered `expected` at a probability of at least `minP`. */
|
|
14
|
+
export const answered = (
|
|
15
|
+
answer: JudgeAnswer | undefined,
|
|
16
|
+
expected: string,
|
|
17
|
+
minP: number,
|
|
18
|
+
): boolean => answer !== undefined && answer.answer === expected && answer.p >= minP
|
|
19
|
+
|
|
20
|
+
/** A numeric var, or `fallback` when it is unset, blank or not a finite number. */
|
|
21
|
+
export const numeric = (value: string | undefined, fallback: number): number => {
|
|
22
|
+
const n = Number(value)
|
|
23
|
+
return value === undefined || value.trim() === "" || !Number.isFinite(n) ? fallback : n
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Whether the last step added or rewrote `path`. */
|
|
27
|
+
export const wrote = (path: string): boolean => changes(path).some((c) => c.status !== "deleted")
|
|
28
|
+
|
|
29
|
+
export interface CheckOptions {
|
|
30
|
+
/** Where a failing run leaves its output. */
|
|
31
|
+
readonly report: string
|
|
32
|
+
readonly label?: string | undefined
|
|
33
|
+
/** Paths removed before the command runs. */
|
|
34
|
+
readonly sweep?: readonly string[] | undefined
|
|
35
|
+
/** Paths removed once it passes. */
|
|
36
|
+
readonly sweepOnGreen?: readonly string[] | undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A step that runs `command` through the driver and keeps its outcome in the
|
|
41
|
+
* tree: a failure writes `report`, a pass removes it. Resolves `true` when the
|
|
42
|
+
* command passed.
|
|
43
|
+
*/
|
|
44
|
+
export const check = async (
|
|
45
|
+
name: string,
|
|
46
|
+
command: string,
|
|
47
|
+
options: CheckOptions,
|
|
48
|
+
): Promise<boolean> => {
|
|
49
|
+
const { report, label, sweep, sweepOnGreen } = options
|
|
50
|
+
await run(
|
|
51
|
+
name,
|
|
52
|
+
checkScript(command, { report, stamp: head().slice(0, 7), sweep, sweepOnGreen }),
|
|
53
|
+
{ label },
|
|
54
|
+
)
|
|
55
|
+
return !wrote(report)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const isCode = (path: string): boolean => path !== ".gtd" && !path.startsWith(".gtd/")
|
|
59
|
+
|
|
60
|
+
/** The last step's changes outside `.gtd/`. */
|
|
61
|
+
export const codeChanges = (): readonly Change[] => changes().filter((c) => isCode(c.path))
|
|
62
|
+
|
|
63
|
+
/** Refuse a turn whose only change deletes `file` — unless it says it found nothing to do. */
|
|
64
|
+
export const requireProgress = (file: string): void => {
|
|
65
|
+
const change = changes().get(file)
|
|
66
|
+
if (change?.status !== "deleted") return
|
|
67
|
+
if (codeChanges().length > 0) return
|
|
68
|
+
if ((change.before ?? "").trim().startsWith("NOTHING ACTIONABLE")) return
|
|
69
|
+
refuse(
|
|
70
|
+
`gtd land: feedback-progress: ${file} was deleted without addressing its instructions — implement the changes it lists (then delete it), don't just remove the file.`,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Refuse a turn that leaves a question in `file` unanswered. An untouched tree is silence, and allowed. */
|
|
75
|
+
export const requireAnswers = (file: string): void => {
|
|
76
|
+
if (changes().length === 0) return
|
|
77
|
+
const open = openQuestions(read(file) ?? "")
|
|
78
|
+
if (open.length === 0) return
|
|
79
|
+
const list = open.map((q) => ` - ${file}:${q.line}: ${q.question}`).join("\n")
|
|
80
|
+
refuse(
|
|
81
|
+
`gtd land: answer-completeness: ${open.length} open question(s) in ${file} not answered — tick exactly one option per question, or delete a question you don't want to answer. To accept the plan as-is instead, revert everything and re-run:\n${list}`,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Refuse a turn that did not undo the code edits `edited` recorded, back to their content before `base`. */
|
|
86
|
+
export const requireRevert = (edited: readonly Change[], base: string): void => {
|
|
87
|
+
const residue = edited.filter((c) => read(c.path) !== c.before).map((c) => c.path)
|
|
88
|
+
if (residue.length === 0) return
|
|
89
|
+
const quoted = residue.map((path) => `'${path.replace(/'/g, "'\\''")}'`).join(" ")
|
|
90
|
+
refuse(
|
|
91
|
+
`gtd land: require-revert: ${residue.join(", ")} still differ from ${base}~1 — the revert did not take. Run \`git checkout ${base}~1 -- ${quoted}\`, then \`gtd land\` again.`,
|
|
92
|
+
)
|
|
93
|
+
}
|