@pmelab/gtd 59.0.1 → 60.1.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 +48 -32
- package/claude/hooks/doors.ts +8 -4
- package/dist/chunks/flows-DENt0uWV.mjs +707 -0
- package/dist/chunks/{steering-CCNvngSH.mjs → steering-DF_BMadH.mjs} +292 -289
- package/dist/chunks/testkit-BhTey1Do.mjs +30517 -0
- package/dist/chunks/{wire-BTUhRGXj.mjs → wire-DZ8R1RU9.mjs} +8 -1
- package/dist/flows.mjs +2 -676
- package/dist/gtd.bundle.mjs +11285 -19864
- package/dist/gtd.core.mjs +593 -0
- package/dist/steering.mjs +2 -2
- package/dist/testkit/gherkin.mjs +246 -37
- package/dist/testkit.mjs +3 -2
- package/dist/web/main.js +234 -232
- package/dist/wire.mjs +2 -2
- package/dist/workflow-kit.mjs +3 -3
- package/package.json +7 -7
- package/skills/authoring/SKILL.md +66 -60
- package/src/cli/Cli.test.ts +185 -184
- package/src/cli/Cli.ts +219 -224
- package/src/cli/History.ts +9 -4
- package/src/cli/Judges.ts +1 -1
- package/src/cli/Output.test.ts +35 -0
- package/src/cli/Output.ts +34 -3
- package/src/cli/Program.test.ts +78 -84
- package/src/cli/Program.ts +192 -128
- package/src/cli/ToolCommands.ts +2 -2
- package/src/edge/Edge.test.ts +72 -62
- package/src/edge/Edge.ts +6 -6
- package/src/flows/factory.test.ts +14 -0
- package/src/flows/factory.ts +22 -5
- package/src/flows/helpers.test.ts +35 -0
- package/src/flows/helpers.ts +19 -10
- package/src/flows/index.test.ts +66 -0
- package/src/flows/index.ts +109 -5
- package/src/flows/internal.ts +7 -0
- package/src/flows/runtime.test.ts +192 -1
- package/src/flows/runtime.ts +189 -55
- package/src/gherkin/git-control.feature +6 -6
- package/src/gherkin/index.test.ts +127 -0
- package/src/gherkin/index.ts +370 -51
- package/src/gherkin/messages.test.ts +38 -4
- package/src/gherkin/messages.ts +21 -4
- package/src/gherkin/more-steps.feature +170 -0
- package/src/gherkin/scenario-end.feature +101 -0
- package/src/gherkin/testkit.feature +83 -7
- package/src/install/Install.test.ts +7 -7
- package/src/install/Install.ts +8 -8
- 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/Replay.test.ts +13 -2
- package/src/replay/Replay.ts +19 -30
- package/src/replay/judgeDiffShape.test.ts +1 -1
- package/src/steering/Footnotes.test.ts +1 -1
- package/src/steering/SteeringFormats.test.ts +1 -1
- package/src/steering/freeform.test.ts +1 -1
- package/src/steering/index.test.ts +27 -1
- package/src/steering/index.ts +6 -104
- package/src/steering/internal.ts +11 -0
- package/src/steering/qa.test.ts +7 -2
- package/src/steering/reads.test.ts +42 -0
- package/src/steering/reads.ts +43 -0
- package/src/steering/registry.ts +93 -0
- package/src/steering/review.test.ts +2 -2
- package/src/step/planStart.test.ts +18 -16
- package/src/step/planStart.ts +12 -12
- package/src/testkit/TestRepo.test.ts +245 -7
- package/src/testkit/TestRepo.ts +271 -40
- package/src/testkit/imports.test.ts +58 -0
- package/src/testkit/imports.ts +102 -0
- package/src/testkit/index.test.ts +28 -0
- package/src/testkit/index.ts +14 -2
- package/src/twins/EmittedScriptRecognizer.ts +2 -6
- package/src/twins/EmittedScripts.test.ts +5 -3
- package/src/twins/GitTiers.ts +6 -7
- package/src/twins/InMemRepo.ts +1 -0
- package/src/twins/Layers.test.ts +1 -1
- package/src/twins/Layers.ts +10 -3
- package/src/twins/cliIo.ts +9 -4
- package/src/wire/Script.ts +11 -0
- package/src/wire/WireSchema.test.ts +3 -0
- package/src/wire/golden/1/script.json +4 -0
- package/src/wire/index.ts +1 -0
- package/src/workflow/SteeringFormats.test.ts +1 -1
- package/src/workflow/compile.ts +2 -2
- package/src/workflow/discovery.test.ts +27 -10
- package/src/workflow/discovery.ts +5 -6
- package/src/workflow/index.ts +1 -0
- package/src/workflow/load.test.ts +316 -119
- package/src/workflow/load.ts +478 -305
- package/src/workflow/pin.test.ts +159 -0
- package/src/workflow/pin.ts +72 -0
- package/src/workflow/vars.ts +3 -3
- package/src/workflow-kit/index.test.ts +25 -0
- package/src/workflow-kit/index.ts +37 -4
- package/src/workflow-kit/review.ts +2 -2
- package/dist/chunks/testkit-07Z_ENch.mjs +0 -32175
package/src/gherkin/messages.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert"
|
|
2
|
-
import type { TestRepo } from "../testkit/index.js"
|
|
2
|
+
import type { BeatDocument, TestRepo } from "../testkit/index.js"
|
|
3
3
|
|
|
4
4
|
const CONTENT_LINES = 5
|
|
5
5
|
const SUBJECTS = 5
|
|
@@ -22,10 +22,19 @@ const beatLines = async (repo: TestRepo): Promise<string[]> => {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/** A scripted step no drive reached, and whether a Background or the scenario scripted it. */
|
|
26
|
+
export interface Unreached {
|
|
27
|
+
readonly step: string
|
|
28
|
+
readonly origin: "Background" | "scenario"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const unreachedList = (steps: readonly Unreached[]): string =>
|
|
32
|
+
steps.map(({ step, origin }) => `"${step}" (${origin})`).join(", ")
|
|
33
|
+
|
|
25
34
|
/** Where the process rests, for a failed outcome to end with. Never throws: it describes a failure. */
|
|
26
35
|
export const restDescription = async (
|
|
27
36
|
repo: () => TestRepo,
|
|
28
|
-
neverUsed: readonly
|
|
37
|
+
neverUsed: readonly Unreached[],
|
|
29
38
|
): Promise<string> => {
|
|
30
39
|
let lines: string[]
|
|
31
40
|
try {
|
|
@@ -41,8 +50,7 @@ export const restDescription = async (
|
|
|
41
50
|
} catch (error) {
|
|
42
51
|
lines = [`unknown — ${reason(error)}`]
|
|
43
52
|
}
|
|
44
|
-
if (neverUsed.length > 0)
|
|
45
|
-
lines.push(`actors never used: ${neverUsed.map((step) => `"${step}"`).join(", ")}`)
|
|
53
|
+
if (neverUsed.length > 0) lines.push(`actors never used: ${unreachedList(neverUsed)}`)
|
|
46
54
|
return ["where the process rests:", ...lines.map((line) => ` ${line}`)].join("\n")
|
|
47
55
|
}
|
|
48
56
|
|
|
@@ -82,3 +90,12 @@ export const assertLacks = (where: string, text: string, content: string | undef
|
|
|
82
90
|
if (content === undefined || content.includes(text))
|
|
83
91
|
assert.fail(containsFailure(where, text, content, false))
|
|
84
92
|
}
|
|
93
|
+
|
|
94
|
+
/** A rest at `step`; a stall there is no rest, so it points to the step that asserts one. */
|
|
95
|
+
export const assertRestsAt = (beat: BeatDocument, step: string): void => {
|
|
96
|
+
if (beat.kind === "stalled")
|
|
97
|
+
assert.fail(
|
|
98
|
+
`expected a rest at "${step}", but the process stalls at "${beat.state}" — assert a stall with: the process stalls at "${beat.state}"`,
|
|
99
|
+
)
|
|
100
|
+
assert.strictEqual(beat.state, step)
|
|
101
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: steps for checks, stalls, beat fields, commit bodies and the environment
|
|
3
|
+
|
|
4
|
+
Background:
|
|
5
|
+
Given a gtd config with:
|
|
6
|
+
"""
|
|
7
|
+
import { agent, check, envSettings, human, scope, settings, workflow } from "@pmelab/gtd/flows"
|
|
8
|
+
|
|
9
|
+
const defaults = { title: "untitled" }
|
|
10
|
+
const envDefaults = { greeting: "hi" }
|
|
11
|
+
|
|
12
|
+
export const tested = workflow(async () => {
|
|
13
|
+
await scope({ name: "build", skills: ["tdd", "review"] }, async () => {
|
|
14
|
+
await agent("plan", "Plan.", { model: "smart" })
|
|
15
|
+
})
|
|
16
|
+
const green = await check("test", "npm test", ".gtd/FEEDBACK.md")
|
|
17
|
+
await agent(green ? "ship" : "repair", `Say ${envSettings.greeting} to ${settings.title}.`)
|
|
18
|
+
await human("done")
|
|
19
|
+
}, { defaults, envDefaults })
|
|
20
|
+
|
|
21
|
+
export default tested
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
Scenario: the resting step's fields are read by name
|
|
25
|
+
When gtd starts workflow "tested"
|
|
26
|
+
Then the process rests at "build.plan"
|
|
27
|
+
And the resting step's model is "smart"
|
|
28
|
+
And the resting step's idle is "false"
|
|
29
|
+
And the resting step's skills includes "review"
|
|
30
|
+
And the resting step's content includes "Plan"
|
|
31
|
+
|
|
32
|
+
Scenario: a quoted argument keeps its spaces
|
|
33
|
+
Given step "build.plan" writes "PLAN.md" with:
|
|
34
|
+
"""
|
|
35
|
+
add two numbers
|
|
36
|
+
"""
|
|
37
|
+
And check step "test" passes
|
|
38
|
+
When gtd starts workflow "tested" with "--var 'title=a calculator'"
|
|
39
|
+
And the process is driven
|
|
40
|
+
Then the resting step's content contains "Say hi to a calculator."
|
|
41
|
+
|
|
42
|
+
Scenario: an agent step that changes nothing stalls
|
|
43
|
+
Given step "build.plan" changes nothing
|
|
44
|
+
When gtd starts workflow "tested"
|
|
45
|
+
And the process is driven
|
|
46
|
+
Then the process stalls at "build.plan"
|
|
47
|
+
|
|
48
|
+
Scenario: a passing check step goes on, and a variable changed mid-process is seen from then on
|
|
49
|
+
Given step "build.plan" writes "PLAN.md" with:
|
|
50
|
+
"""
|
|
51
|
+
add two numbers
|
|
52
|
+
"""
|
|
53
|
+
And check step "test" passes
|
|
54
|
+
When gtd starts workflow "tested"
|
|
55
|
+
And the process is driven
|
|
56
|
+
Then the process rests at "ship"
|
|
57
|
+
And the resting step's content contains "Say hi to untitled."
|
|
58
|
+
Given from now on the environment variable "GTD_GREETING" is "hello"
|
|
59
|
+
Then the resting step's content contains "Say hello to untitled."
|
|
60
|
+
|
|
61
|
+
Scenario: a failing check step writes its report
|
|
62
|
+
Given the environment variable "GTD_GREETING" is "hey"
|
|
63
|
+
And step "build.plan" writes "PLAN.md" with:
|
|
64
|
+
"""
|
|
65
|
+
add two numbers
|
|
66
|
+
"""
|
|
67
|
+
And check step "test" fails with:
|
|
68
|
+
"""
|
|
69
|
+
1 test failing
|
|
70
|
+
"""
|
|
71
|
+
When gtd starts workflow "tested"
|
|
72
|
+
And the process is driven
|
|
73
|
+
Then the process rests at "repair"
|
|
74
|
+
And ".gtd/FEEDBACK.md" contains "1 test failing"
|
|
75
|
+
And the resting step's content contains "Say hey to untitled."
|
|
76
|
+
|
|
77
|
+
Scenario: a check step scripted at a step that is no check refuses the drive
|
|
78
|
+
Given check step "build.plan" passes
|
|
79
|
+
When gtd starts workflow "tested"
|
|
80
|
+
Then driving the process is refused with 'step "build.plan" is no check step; its actor is agent'
|
|
81
|
+
|
|
82
|
+
Scenario: commit bodies are read whole, every commit of a subject at once
|
|
83
|
+
Given the working tree is committed with the message:
|
|
84
|
+
"""
|
|
85
|
+
chore: seed
|
|
86
|
+
|
|
87
|
+
Seeded by hand.
|
|
88
|
+
|
|
89
|
+
Refs: #1
|
|
90
|
+
"""
|
|
91
|
+
And step "build.plan" writes "PLAN.md" with:
|
|
92
|
+
"""
|
|
93
|
+
add two numbers
|
|
94
|
+
"""
|
|
95
|
+
And check step "test" passes
|
|
96
|
+
When gtd starts workflow "tested"
|
|
97
|
+
And the process is driven
|
|
98
|
+
Then the last commit body contains "Gtd-Step: test#1"
|
|
99
|
+
And the commit "gtd(agent): build.plan → test" has a body containing "Gtd-Step: build.plan#1"
|
|
100
|
+
And the commit "chore: seed" has a body containing "Seeded by hand."
|
|
101
|
+
And no commit body contains "never written"
|
|
102
|
+
|
|
103
|
+
Scenario: a canned command matched by pattern, a branch at a ref, and a file as committed there
|
|
104
|
+
Given a file "a.txt" with:
|
|
105
|
+
"""
|
|
106
|
+
one
|
|
107
|
+
"""
|
|
108
|
+
And the working tree is committed as "chore: one"
|
|
109
|
+
And a file "a.txt" with:
|
|
110
|
+
"""
|
|
111
|
+
two
|
|
112
|
+
"""
|
|
113
|
+
And the working tree is committed as "chore: two"
|
|
114
|
+
And a branch "old" at "HEAD~1"
|
|
115
|
+
And the command matching "^npm (test|run)" exits 1 with:
|
|
116
|
+
"""
|
|
117
|
+
red
|
|
118
|
+
"""
|
|
119
|
+
And step "build.plan" writes "PLAN.md" with:
|
|
120
|
+
"""
|
|
121
|
+
add two numbers
|
|
122
|
+
"""
|
|
123
|
+
And step "test" runs its script
|
|
124
|
+
When gtd starts workflow "tested"
|
|
125
|
+
And the process is driven
|
|
126
|
+
Then the process rests at "repair"
|
|
127
|
+
And "a.txt" as committed at "old" contains "one"
|
|
128
|
+
And ".gtd/FEEDBACK.md" contains "red"
|
|
129
|
+
|
|
130
|
+
Scenario: text is appended to a file, and a step overwrites one only if it exists
|
|
131
|
+
Given a file "NOTES.md" with:
|
|
132
|
+
"""
|
|
133
|
+
first
|
|
134
|
+
"""
|
|
135
|
+
And text is appended to "NOTES.md":
|
|
136
|
+
"""
|
|
137
|
+
second
|
|
138
|
+
"""
|
|
139
|
+
And step "build.plan" overwrites "PLAN.md", if it exists, with:
|
|
140
|
+
"""
|
|
141
|
+
never written
|
|
142
|
+
"""
|
|
143
|
+
And step "build.plan" overwrites "NOTES.md", if it exists, with:
|
|
144
|
+
"""
|
|
145
|
+
rewritten
|
|
146
|
+
"""
|
|
147
|
+
And step "test" runs its script
|
|
148
|
+
And the command "npm test" exits 0
|
|
149
|
+
When gtd starts workflow "tested"
|
|
150
|
+
And the process is driven
|
|
151
|
+
Then the process rests at "ship"
|
|
152
|
+
And "NOTES.md" contains "rewritten"
|
|
153
|
+
And "PLAN.md" does not exist
|
|
154
|
+
|
|
155
|
+
Scenario: a config given as source imports a module the scenario names
|
|
156
|
+
Given the module "src/gherkin/tiny.fixture.ts" is importable as "#workflow"
|
|
157
|
+
And a gtd config with:
|
|
158
|
+
"""
|
|
159
|
+
export { tiny, tiny as default } from "#workflow"
|
|
160
|
+
"""
|
|
161
|
+
And a file "IDEA.md" with:
|
|
162
|
+
"""
|
|
163
|
+
a calculator
|
|
164
|
+
"""
|
|
165
|
+
And step "plan" writes "PLAN.md" with:
|
|
166
|
+
"""
|
|
167
|
+
add two numbers
|
|
168
|
+
"""
|
|
169
|
+
When the process is driven
|
|
170
|
+
Then the process rests at "sign-off"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
@inmem
|
|
2
|
+
Feature: a scenario's scripted steps must be reached by its end, not by every drive
|
|
3
|
+
|
|
4
|
+
Background:
|
|
5
|
+
Given a gtd config with:
|
|
6
|
+
"""
|
|
7
|
+
import { agent, human, workflow } from "@pmelab/gtd/flows"
|
|
8
|
+
|
|
9
|
+
export const shipping = workflow(async () => {
|
|
10
|
+
await agent("plan", "Write PLAN.md.")
|
|
11
|
+
await human("sign-off")
|
|
12
|
+
await agent("ship", "Ship it.")
|
|
13
|
+
await human("done")
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export default shipping
|
|
17
|
+
"""
|
|
18
|
+
And step "ship" writes "SHIPPED.md" with:
|
|
19
|
+
"""
|
|
20
|
+
shipped
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
Scenario: an actor the Background scripts, reached only by the second drive, passes
|
|
24
|
+
Given step "plan" writes "PLAN.md" with:
|
|
25
|
+
"""
|
|
26
|
+
add two numbers
|
|
27
|
+
"""
|
|
28
|
+
When gtd starts workflow "shipping"
|
|
29
|
+
And the process is driven
|
|
30
|
+
Then the process rests at "sign-off"
|
|
31
|
+
Given step "sign-off" writes "SIGNED.md" with:
|
|
32
|
+
"""
|
|
33
|
+
signed
|
|
34
|
+
"""
|
|
35
|
+
When the process is driven
|
|
36
|
+
Then the process rests at "done"
|
|
37
|
+
And "SHIPPED.md" contains "shipped"
|
|
38
|
+
|
|
39
|
+
Scenario: an actor the Background scripts may go unreached in a scenario another scenario reaches it in
|
|
40
|
+
Given step "plan" writes "PLAN.md" with:
|
|
41
|
+
"""
|
|
42
|
+
add two numbers
|
|
43
|
+
"""
|
|
44
|
+
When gtd starts workflow "shipping"
|
|
45
|
+
And the process is driven
|
|
46
|
+
Then the process rests at "sign-off"
|
|
47
|
+
|
|
48
|
+
@fails
|
|
49
|
+
Scenario: an actor the scenario scripts that no drive reaches fails it at its end
|
|
50
|
+
Given step "plan" writes "PLAN.md" with:
|
|
51
|
+
"""
|
|
52
|
+
add two numbers
|
|
53
|
+
"""
|
|
54
|
+
And step "done" writes "DONE.md" with:
|
|
55
|
+
"""
|
|
56
|
+
done
|
|
57
|
+
"""
|
|
58
|
+
When gtd starts workflow "shipping"
|
|
59
|
+
And the process is driven
|
|
60
|
+
Then the process rests at "sign-off"
|
|
61
|
+
|
|
62
|
+
@fails
|
|
63
|
+
Scenario: a lenient drive leaves an actor no drive reaches to fail the scenario at its end
|
|
64
|
+
Given step "plan" writes "PLAN.md" with:
|
|
65
|
+
"""
|
|
66
|
+
add two numbers
|
|
67
|
+
"""
|
|
68
|
+
And step "done" writes "DONE.md" with:
|
|
69
|
+
"""
|
|
70
|
+
done
|
|
71
|
+
"""
|
|
72
|
+
When gtd starts workflow "shipping"
|
|
73
|
+
And the process is driven leniently
|
|
74
|
+
Then the process rests at "sign-off"
|
|
75
|
+
|
|
76
|
+
Scenario: a refused drive excuses the actors it was handed
|
|
77
|
+
Given step "plan" writes "PLAN.md" with:
|
|
78
|
+
"""
|
|
79
|
+
add two numbers
|
|
80
|
+
"""
|
|
81
|
+
And step "sign-off" changes nothing
|
|
82
|
+
And step "done" writes "DONE.md" with:
|
|
83
|
+
"""
|
|
84
|
+
done
|
|
85
|
+
"""
|
|
86
|
+
When gtd starts workflow "shipping"
|
|
87
|
+
Then driving the process is refused with 'the actor for human step "sign-off" changed nothing'
|
|
88
|
+
|
|
89
|
+
@fails
|
|
90
|
+
Scenario: an actor scripted after a refused drive, and reached by no later one, fails the scenario
|
|
91
|
+
Given step "plan" writes "PLAN.md" with:
|
|
92
|
+
"""
|
|
93
|
+
add two numbers
|
|
94
|
+
"""
|
|
95
|
+
And step "sign-off" changes nothing
|
|
96
|
+
When gtd starts workflow "shipping"
|
|
97
|
+
Then driving the process is refused with 'the actor for human step "sign-off" changed nothing'
|
|
98
|
+
Given step "done" writes "DONE.md" with:
|
|
99
|
+
"""
|
|
100
|
+
done
|
|
101
|
+
"""
|
|
@@ -7,9 +7,9 @@ Feature: testkit drives any workflow against the real engine
|
|
|
7
7
|
Background:
|
|
8
8
|
Given a gtd config with:
|
|
9
9
|
"""
|
|
10
|
-
import { agent, human, judge, read, refuse, run } from "@pmelab/gtd/flows"
|
|
10
|
+
import { agent, human, judge, read, refuse, run, workflow } from "@pmelab/gtd/flows"
|
|
11
11
|
|
|
12
|
-
export const tiny = async () => {
|
|
12
|
+
export const tiny = workflow(async () => {
|
|
13
13
|
await agent("plan", "Write PLAN.md.")
|
|
14
14
|
await run("check", "npm test > .gtd/FEEDBACK.md 2>&1 && rm .gtd/FEEDBACK.md")
|
|
15
15
|
if (read(".gtd/FEEDBACK.md") !== undefined) await agent("repair", "Make the suite pass.")
|
|
@@ -18,11 +18,11 @@ Feature: testkit drives any workflow against the real engine
|
|
|
18
18
|
evidence: { plan: read("PLAN.md") ?? "" },
|
|
19
19
|
})
|
|
20
20
|
await human(answers.ready?.answer === "yes" ? "ship" : "rework")
|
|
21
|
-
}
|
|
21
|
+
})
|
|
22
22
|
|
|
23
|
-
export const picky = async () => {
|
|
23
|
+
export const picky = workflow(async () => {
|
|
24
24
|
refuse("gtd: picky refuses every start")
|
|
25
|
-
}
|
|
25
|
+
})
|
|
26
26
|
|
|
27
27
|
export default tiny
|
|
28
28
|
"""
|
|
@@ -83,9 +83,12 @@ Feature: testkit drives any workflow against the real engine
|
|
|
83
83
|
And "PLAN.md" contains "add two numbers"
|
|
84
84
|
|
|
85
85
|
Scenario: a judge step with queued actions refuses the drive
|
|
86
|
-
Given step "plan"
|
|
86
|
+
Given step "plan" writes "PLAN.md" with:
|
|
87
|
+
"""
|
|
88
|
+
add two numbers
|
|
89
|
+
"""
|
|
87
90
|
And step "check" changes nothing
|
|
88
|
-
And step "ready"
|
|
91
|
+
And step "ready" changes nothing
|
|
89
92
|
And judge step "ready" answers:
|
|
90
93
|
"""
|
|
91
94
|
[{ "id": "ready", "answer": true, "p": 0.9 }]
|
|
@@ -98,6 +101,79 @@ Feature: testkit drives any workflow against the real engine
|
|
|
98
101
|
When gtd starts workflow "tiny"
|
|
99
102
|
Then driving the process is refused with 'was not prompted with "Write a poem"'
|
|
100
103
|
|
|
104
|
+
Scenario: a step only asserted to be prompted is where the drive rests, and nothing lands there
|
|
105
|
+
Given step "plan" is prompted with "Write PLAN.md."
|
|
106
|
+
When gtd starts workflow "tiny"
|
|
107
|
+
And the process is driven
|
|
108
|
+
Then the process rests at "plan"
|
|
109
|
+
And the last commit subject is "gtd(human): plan"
|
|
110
|
+
|
|
111
|
+
Scenario: a prompt asserted beside a script is checked at every visit, before the step acts
|
|
112
|
+
Given step "check" is prompted with "npm test"
|
|
113
|
+
And step "check" writes ".gtd/FEEDBACK.md" with:
|
|
114
|
+
"""
|
|
115
|
+
1 test failing
|
|
116
|
+
"""
|
|
117
|
+
And step "plan" writes "PLAN.md" with:
|
|
118
|
+
"""
|
|
119
|
+
add two numbers
|
|
120
|
+
"""
|
|
121
|
+
When gtd starts workflow "tiny"
|
|
122
|
+
And the process is driven
|
|
123
|
+
Then the process rests at "repair"
|
|
124
|
+
|
|
125
|
+
@fails
|
|
126
|
+
Scenario: a prompt asserted at a step no drive reaches fails the scenario at its end
|
|
127
|
+
Given step "plan" changes nothing
|
|
128
|
+
And step "repair" is prompted with "Make the suite pass."
|
|
129
|
+
When gtd starts workflow "tiny"
|
|
130
|
+
And the process is driven
|
|
131
|
+
Then the process stalls at "plan"
|
|
132
|
+
|
|
133
|
+
Scenario: a human step scripted to change nothing refuses the drive instead of repeating it
|
|
134
|
+
Given step "plan" writes "PLAN.md" with:
|
|
135
|
+
"""
|
|
136
|
+
add two numbers
|
|
137
|
+
"""
|
|
138
|
+
And step "check" changes nothing
|
|
139
|
+
And judge step "ready" answers:
|
|
140
|
+
"""
|
|
141
|
+
[{ "id": "ready", "answer": true, "p": 0.9 }]
|
|
142
|
+
"""
|
|
143
|
+
And step "ship" changes nothing
|
|
144
|
+
When gtd starts workflow "tiny"
|
|
145
|
+
Then driving the process is refused with 'the actor for human step "ship" changed nothing'
|
|
146
|
+
|
|
147
|
+
Scenario: a lenient drive rests at an unscripted check step
|
|
148
|
+
Given step "plan" writes "PLAN.md" with:
|
|
149
|
+
"""
|
|
150
|
+
add two numbers
|
|
151
|
+
"""
|
|
152
|
+
When gtd starts workflow "tiny"
|
|
153
|
+
And the process is driven leniently
|
|
154
|
+
Then the process rests at "check"
|
|
155
|
+
And the resting step's content contains "npm test"
|
|
156
|
+
|
|
157
|
+
Scenario: a step scripted for a later drive counts once any drive reached it
|
|
158
|
+
Given step "plan" writes "PLAN.md" with:
|
|
159
|
+
"""
|
|
160
|
+
add two numbers
|
|
161
|
+
"""
|
|
162
|
+
And step "check" writes ".gtd/FEEDBACK.md" with:
|
|
163
|
+
"""
|
|
164
|
+
1 test failing
|
|
165
|
+
"""
|
|
166
|
+
When gtd starts workflow "tiny"
|
|
167
|
+
And the process is driven
|
|
168
|
+
Then the process rests at "repair"
|
|
169
|
+
Given step "repair" deletes ".gtd/FEEDBACK.md"
|
|
170
|
+
And judge step "ready" answers:
|
|
171
|
+
"""
|
|
172
|
+
[{ "id": "ready", "answer": false, "p": 0.9 }]
|
|
173
|
+
"""
|
|
174
|
+
When the process is driven
|
|
175
|
+
Then the process rests at "rework"
|
|
176
|
+
|
|
101
177
|
Scenario: a start the workflow refuses is reported with its message
|
|
102
178
|
Then starting workflow "picky" with "--var mode=bad" is refused with "picky refuses every start"
|
|
103
179
|
|
|
@@ -164,8 +164,8 @@ describe("renderBriefing", () => {
|
|
|
164
164
|
|
|
165
165
|
it("contains the door invocation, exec'ing the loop", () => {
|
|
166
166
|
const briefing = renderBriefing()
|
|
167
|
-
expect(briefing).toContain('gtd
|
|
168
|
-
expect(briefing).
|
|
167
|
+
expect(briefing).toContain('gtd start "$@"')
|
|
168
|
+
expect(briefing).toMatch(/bare `gtd start` lists/i)
|
|
169
169
|
expect(briefing).toMatch(/exec.{0,20}GTD_BUILD/s)
|
|
170
170
|
})
|
|
171
171
|
|
|
@@ -418,19 +418,19 @@ describe("DOOR_COMMAND", () => {
|
|
|
418
418
|
|
|
419
419
|
it("cds to the repository root before any gtd call", () => {
|
|
420
420
|
const cdIndex = DOOR_COMMAND.indexOf('cd "$(git rev-parse --show-toplevel)"')
|
|
421
|
-
const gtdIndex = DOOR_COMMAND.indexOf("gtd
|
|
421
|
+
const gtdIndex = DOOR_COMMAND.indexOf("gtd start")
|
|
422
422
|
expect(cdIndex).toBeGreaterThan(-1)
|
|
423
423
|
expect(gtdIndex).toBeGreaterThan(cdIndex)
|
|
424
424
|
})
|
|
425
425
|
|
|
426
|
-
it("hands every argument to gtd
|
|
427
|
-
expect(DOOR_COMMAND).toContain('gtd
|
|
426
|
+
it("hands every argument to gtd start", () => {
|
|
427
|
+
expect(DOOR_COMMAND).toContain('gtd start "$@"')
|
|
428
428
|
})
|
|
429
429
|
|
|
430
430
|
it("captures the emitted script via command substitution, never a pipe into sh", () => {
|
|
431
|
-
expect(DOOR_COMMAND).toContain('script="$(gtd
|
|
431
|
+
expect(DOOR_COMMAND).toContain('script="$(gtd start "$@")"')
|
|
432
432
|
expect(DOOR_COMMAND).toContain('sh -c "$script"')
|
|
433
|
-
expect(DOOR_COMMAND).not.toMatch(/gtd
|
|
433
|
+
expect(DOOR_COMMAND).not.toMatch(/gtd start[^\n]*\|\s*sh/)
|
|
434
434
|
})
|
|
435
435
|
|
|
436
436
|
it("prints usage to stderr and exits 2 when no name is given", () => {
|
package/src/install/Install.ts
CHANGED
|
@@ -175,7 +175,7 @@ if [ -z "\${1:-}" ]; then
|
|
|
175
175
|
exit 2
|
|
176
176
|
fi
|
|
177
177
|
cd "$(git rev-parse --show-toplevel)"
|
|
178
|
-
script="$(gtd
|
|
178
|
+
script="$(gtd start "$@")"
|
|
179
179
|
sh -c "$script"
|
|
180
180
|
exec "$GTD_BUILD"`
|
|
181
181
|
|
|
@@ -365,7 +365,7 @@ const DRIVER_OBLIGATIONS = `
|
|
|
365
365
|
\`--json=judge\`, pipes that rendered \`{ schema, state, questions }\` document to a
|
|
366
366
|
judge model, and pipes ITS verdict (one \`{ id, answer, p }\` per pending
|
|
367
367
|
question, as JSON on stdin) to \`gtd judge answer\` — the FIFTH command
|
|
368
|
-
that changes anything, alongside \`gtd land\`/\`gtd
|
|
368
|
+
that changes anything, alongside \`gtd land\`/\`gtd start\`/\`gtd
|
|
369
369
|
abandon\`/\`gtd restore\`. It follows the exact same required-half /
|
|
370
370
|
optional-half script contract as \`gtd land\` in obligation 8 above:
|
|
371
371
|
read \`--json=script\` (and, if you want them, \`--json=settled\`/
|
|
@@ -565,9 +565,9 @@ ${EDIT_COMMAND}
|
|
|
565
565
|
|
|
566
566
|
### \`gtd-door <name> [args...]\` — start a process through a door and drive it
|
|
567
567
|
|
|
568
|
-
Runs \`gtd
|
|
568
|
+
Runs \`gtd start <name> [args...]\`, captured by command substitution — never a
|
|
569
569
|
pipe: a pipeline reports only its LAST command's exit status, so
|
|
570
|
-
\`gtd
|
|
570
|
+
\`gtd start ... | sh\` under \`set -e\` would sail past a refusal and \`exec\` a
|
|
571
571
|
loop over a process that was never started. It runs the captured script, then
|
|
572
572
|
\`exec\`s the suite's RESOLVED \`gtd-build\` path — never the literal string
|
|
573
573
|
\`gtd-build\`. This is one invocation that carries the process all the way to
|
|
@@ -575,9 +575,9 @@ its next human gate, not just a starter: \`gtd-door review\` on a RED baseline
|
|
|
575
575
|
hands off to \`gtd-build\`, which halts at the blocked gate and prints it,
|
|
576
576
|
rather than starting the review; \`gtd-door fix\` on a GREEN suite is a no-op
|
|
577
577
|
straight back to \`idle\` — the exec'd \`gtd-build\` exits immediately on it.
|
|
578
|
-
\`gtd
|
|
579
|
-
|
|
580
|
-
\`review [base]\`
|
|
578
|
+
A bare \`gtd start\` lists what a repository can start: its workflows, and
|
|
579
|
+
the doors those workflows bring (gtd-workflows' \`default\` brings \`fix\`
|
|
580
|
+
and \`review [base]\`) plus any the config declares. Refuses with
|
|
581
581
|
\`usage: gtd-door <name> [args...]\` on stderr and exit \`2\` when the name is
|
|
582
582
|
missing — nothing was even attempted. Default path: \`~/.local/bin/gtd-door\`.
|
|
583
583
|
|
|
@@ -682,7 +682,7 @@ const PREREQUISITES = `
|
|
|
682
682
|
## Prerequisites and portability
|
|
683
683
|
|
|
684
684
|
- A POSIX \`sh\` (dash, ash, bash's own POSIX mode, etc.) — gtd's own emitted
|
|
685
|
-
scripts (\`gtd land --json=script\`, \`gtd
|
|
685
|
+
scripts (\`gtd land --json=script\`, \`gtd start <name>\`, \`gtd abandon\`,
|
|
686
686
|
\`gtd restore\`, \`gtd judge answer --json=script\`) are POSIX sh; captured,
|
|
687
687
|
then piped into it (see obligation 8 above). Reading
|
|
688
688
|
\`gtd next --json=<path>\`/\`gtd land --json=<path>\`'s own output needs
|
package/src/install/Version.ts
CHANGED
|
@@ -22,6 +22,12 @@ const findPackageJson = (fromUrl: string): string => {
|
|
|
22
22
|
throw new Error(`package.json not found by walking up from ${fromUrl}`)
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
/** The global a pin's workflow registers itself under, ahead of the engine in the same file. */
|
|
26
|
+
export const PIN_KEY = Symbol.for("@pmelab/gtd/pin")
|
|
27
|
+
|
|
28
|
+
// Inside a pin, `import.meta.url` is the pin's own file, in the user's
|
|
29
|
+
// repository: the pin registered the version it was built by before this runs.
|
|
30
|
+
const pinned = (globalThis as { [PIN_KEY]?: { readonly version: string } })[PIN_KEY]?.version
|
|
31
|
+
|
|
32
|
+
export const GTD_VERSION: string =
|
|
33
|
+
pinned ?? (_require(findPackageJson(import.meta.url)) as { version: string }).version
|
package/src/install/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { renderBriefing, WORKFLOWS_REPO } from "./Install.js"
|
|
2
|
-
export { GTD_VERSION } from "./Version.js"
|
|
2
|
+
export { GTD_VERSION, PIN_KEY } from "./Version.js"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Command, CommandExecutor } from "@effect/platform"
|
|
2
1
|
import { Context, Effect, Layer, Stream } from "effect"
|
|
2
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process"
|
|
3
3
|
import { Host } from "./Host.js"
|
|
4
4
|
|
|
5
5
|
/** One command run's outcome: its exit status (a signal death reported as `null`, matching `spawnSync`) and its combined output (stdout then stderr). */
|
|
@@ -23,13 +23,13 @@ interface CommandOutcome {
|
|
|
23
23
|
* stdout/stderr, none of which this port's single-shot "run to completion"
|
|
24
24
|
* shape supports.
|
|
25
25
|
*/
|
|
26
|
-
export class CommandRunner extends Context.
|
|
26
|
+
export class CommandRunner extends Context.Service<
|
|
27
27
|
CommandRunner,
|
|
28
28
|
{
|
|
29
29
|
/** A SPAWN failure (no `bash`, unreadable cwd) fails the Effect; a non-zero EXIT is a value — how a mode command reports "invalid"/"broken" for the caller to interpret. */
|
|
30
30
|
readonly bash: (command: string) => Effect.Effect<CommandOutcome, Error>
|
|
31
31
|
}
|
|
32
|
-
>() {
|
|
32
|
+
>()("CommandRunner") {
|
|
33
33
|
/** A test layer over a canned `bash` implementation — no subprocess. */
|
|
34
34
|
static readonly layer = (
|
|
35
35
|
bash: (command: string) => Effect.Effect<CommandOutcome, Error>,
|
|
@@ -39,17 +39,19 @@ export class CommandRunner extends Context.Tag("CommandRunner")<
|
|
|
39
39
|
CommandRunner,
|
|
40
40
|
Effect.gen(function* () {
|
|
41
41
|
const { root } = yield* Host
|
|
42
|
-
const
|
|
42
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
|
43
43
|
return {
|
|
44
44
|
bash: (command: string) =>
|
|
45
45
|
Effect.scoped(
|
|
46
46
|
Effect.gen(function* () {
|
|
47
|
-
const process = yield*
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
const process = yield* spawner.spawn(
|
|
48
|
+
ChildProcess.make("bash", ["-c", command], {
|
|
49
|
+
cwd: root,
|
|
50
|
+
stdout: "pipe",
|
|
51
|
+
stderr: "pipe",
|
|
52
|
+
// Not detached, like `Git.ts`'s `run`: a background job the command starts survives it.
|
|
53
|
+
detached: false,
|
|
54
|
+
}),
|
|
53
55
|
)
|
|
54
56
|
const collect = (stream: typeof process.stdout) =>
|
|
55
57
|
stream.pipe(Stream.decodeText(), Stream.mkString)
|
|
@@ -60,7 +62,7 @@ export class CommandRunner extends Context.Tag("CommandRunner")<
|
|
|
60
62
|
// A signal death fails process.exitCode; map to null to match spawnSync's contract.
|
|
61
63
|
process.exitCode.pipe(
|
|
62
64
|
Effect.map((code): number | null => code),
|
|
63
|
-
Effect.
|
|
65
|
+
Effect.catch(() => Effect.succeed(null)),
|
|
64
66
|
),
|
|
65
67
|
],
|
|
66
68
|
{ concurrency: "unbounded" },
|
|
@@ -5,13 +5,13 @@ import { Context, Effect, Layer } from "effect"
|
|
|
5
5
|
* layer was built, never by the call site; `warn` is its ungated
|
|
6
6
|
* counterpart, always written regardless of verbosity.
|
|
7
7
|
*/
|
|
8
|
-
export class Narrator extends Context.
|
|
8
|
+
export class Narrator extends Context.Service<
|
|
9
9
|
Narrator,
|
|
10
10
|
{
|
|
11
11
|
readonly narrate: (line: string) => Effect.Effect<void>
|
|
12
12
|
readonly warn: (line: string) => Effect.Effect<void>
|
|
13
13
|
}
|
|
14
|
-
>() {
|
|
14
|
+
>()("Narrator") {
|
|
15
15
|
/**
|
|
16
16
|
* `write` is the raw stderr sink (`CliIo.stderr` in production, so the
|
|
17
17
|
* in-memory `CliIo` used by `@inmem` e2e scenarios captures narration into
|
package/src/platform/Git.test.ts
CHANGED
|
@@ -2,11 +2,10 @@ import { execFileSync } from "node:child_process"
|
|
|
2
2
|
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs"
|
|
3
3
|
import { tmpdir } from "node:os"
|
|
4
4
|
import { join } from "node:path"
|
|
5
|
-
import { NodeContext } from "@effect/platform-node"
|
|
6
5
|
import { Cause, Effect, Exit, Option } from "effect"
|
|
7
6
|
import { describe, expect, it } from "vitest"
|
|
8
7
|
import { GitService, InvalidRefError, resolvedRefOrCorrupted } from "./Git.js"
|
|
9
|
-
import { Host } from "./index.js"
|
|
8
|
+
import { Host, NodeServices } from "./index.js"
|
|
10
9
|
import { gitTiers, runGitServiceContract, runGitScriptContract } from "../twins/index.js"
|
|
11
10
|
|
|
12
11
|
/**
|
|
@@ -78,7 +77,7 @@ describe("diffPaths [Live]", () => {
|
|
|
78
77
|
Effect.flatMap(GitService, (g) => g.diffPaths(base, tip)).pipe(
|
|
79
78
|
Effect.provide(GitService.Live),
|
|
80
79
|
Effect.provide(Host.layer({ root: join(root, "sub"), home: root, env: {} })),
|
|
81
|
-
Effect.provide(
|
|
80
|
+
Effect.provide(NodeServices.layer),
|
|
82
81
|
),
|
|
83
82
|
)
|
|
84
83
|
expect([...paths].sort()).toEqual(["sub/a.txt", "top.txt"])
|
|
@@ -131,7 +130,7 @@ describe("diffPaths [Live]", () => {
|
|
|
131
130
|
Effect.flatMap(GitService, (g) => g.diffPaths(base, tip)).pipe(
|
|
132
131
|
Effect.provide(GitService.Live),
|
|
133
132
|
Effect.provide(Host.layer({ root: repo, home: root, env: {} })),
|
|
134
|
-
Effect.provide(
|
|
133
|
+
Effect.provide(NodeServices.layer),
|
|
135
134
|
),
|
|
136
135
|
)
|
|
137
136
|
expect([...paths]).toEqual(["sub"])
|
|
@@ -155,7 +154,7 @@ describe("authorIdent [Live]", () => {
|
|
|
155
154
|
Effect.flatMap(GitService, (g) => g.authorIdent()).pipe(
|
|
156
155
|
Effect.provide(GitService.Live),
|
|
157
156
|
Effect.provide(Host.layer({ root, home: root, env: {} })),
|
|
158
|
-
Effect.provide(
|
|
157
|
+
Effect.provide(NodeServices.layer),
|
|
159
158
|
),
|
|
160
159
|
)
|
|
161
160
|
expect(Option.isNone(ident)).toBe(true)
|