@pmelab/gtd 24.2.0 → 24.4.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 +41 -6
- package/dist/chunks/{Schema-Dh-mkwkq.mjs → Schema-B5yK7QrT.mjs} +47 -1
- package/dist/chunks/{reserved-ejn-LVYO.mjs → reserved-B9pG93Fc.mjs} +53 -13
- package/dist/chunks/{steering-D-PvRC5W.mjs → steering-DVILanVf.mjs} +16 -5
- package/dist/flows.mjs +9 -1
- package/dist/gtd.bundle.mjs +1114 -865
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +1135 -218
- package/dist/workflow-kit.mjs +9 -7
- package/dist/workflows/feature.mjs +152 -35
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +1 -1
- package/dist/workflows.mjs +223 -54
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +57 -0
- package/src/cli/Cli.ts +89 -9
- package/src/cli/History.test.ts +225 -0
- package/src/cli/History.ts +143 -0
- package/src/cli/Output.ts +68 -0
- package/src/cli/Program.test.ts +10 -9
- package/src/cli/Program.ts +24 -72
- package/src/edge/Edge.test.ts +261 -3
- package/src/edge/Edge.ts +218 -41
- package/src/edge/index.ts +1 -0
- package/src/emit/GitScript.test.ts +27 -0
- package/src/emit/GitScript.ts +18 -2
- package/src/emit/index.ts +2 -0
- package/src/flows/scripts.ts +6 -0
- package/src/platform/Git.test.ts +6 -9
- package/src/platform/Git.ts +48 -10
- package/src/platform/RetainedHistory.test.ts +14 -0
- package/src/platform/RetainedHistory.ts +5 -0
- package/src/platform/index.ts +7 -1
- package/src/replay/Trailers.test.ts +32 -0
- package/src/replay/Trailers.ts +17 -0
- package/src/steering/FORMATS.md +1 -1
- package/src/steering/SteeringFormat.test.ts +9 -1
- package/src/steering/SteeringFormat.ts +7 -0
- package/src/steering/contract.test.ts +3 -0
- package/src/steering/index.ts +1 -0
- package/src/steering/review.test.ts +30 -0
- package/src/steering/review.ts +8 -2
- package/src/step/LandStep.ts +3 -1
- package/src/step/RepoSnapshot.ts +7 -1
- package/src/step/planStart.test.ts +6 -3
- package/src/step/planStep.test.ts +29 -1
- package/src/step/planStep.ts +8 -0
- package/src/testkit/EmittedScriptRecognizer.test.ts +29 -30
- package/src/testkit/EmittedScriptRecognizer.ts +149 -52
- package/src/testkit/EmittedScripts.test.ts +166 -0
- package/src/testkit/FakeGitOperations.ts +6 -2
- package/src/testkit/FlowScripts.test.ts +242 -0
- package/src/testkit/FlowScripts.ts +326 -0
- package/src/testkit/GitTiers.ts +269 -47
- package/src/testkit/InMemRepo.ts +61 -8
- package/src/testkit/Layers.ts +10 -2
- package/src/testkit/TestRepo.ts +4 -1
- package/src/testkit/index.ts +1 -1
- package/src/workflow/discovery.test.ts +147 -0
- package/src/workflow/discovery.ts +5 -0
- package/src/workflow/fingerprint.test.ts +20 -4
- package/src/workflow/fingerprint.ts +8 -9
- package/src/workflow/index.ts +1 -6
- package/src/workflow/load.test.ts +5 -19
- package/src/workflow/load.ts +94 -10
- package/src/workflow-kit/review.test.ts +84 -0
- package/src/workflow-kit/review.ts +9 -6
- package/src/workflows/feature/review.test.ts +238 -12
- package/src/workflows/feature/review.ts +172 -31
- package/src/workflows/feature/steps.test.ts +12 -0
- package/src/workflows/feature/steps.ts +8 -1
- package/src/workflows/shared/access.test.ts +4 -2
- package/src/workflows/shared/access.ts +1 -0
- package/src/workflows/shared/options.test.ts +8 -0
- package/src/workflows/shared/options.ts +1 -0
- package/src/workflows/shared/steps.ts +2 -0
- package/src/workflows/shared/tail.ts +3 -11
- package/src/workflows/shared/text.test.ts +106 -0
- package/src/workflows/shared/text.ts +57 -1
- package/src/workflows/shared/vars.ts +1 -0
package/src/cli/Program.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Effect, Option, Schema } from "effect"
|
|
2
2
|
import {
|
|
3
|
-
CommandRunner,
|
|
4
3
|
GtdUsageError,
|
|
5
4
|
Narrator,
|
|
6
5
|
GitService,
|
|
@@ -67,7 +66,7 @@ import {
|
|
|
67
66
|
openThreads,
|
|
68
67
|
parseCodeThreads,
|
|
69
68
|
unansweredQuestions,
|
|
70
|
-
|
|
69
|
+
formatFinding,
|
|
71
70
|
type SteeringFormat,
|
|
72
71
|
} from "../steering/index.js"
|
|
73
72
|
import {
|
|
@@ -89,6 +88,20 @@ import {
|
|
|
89
88
|
} from "../emit/index.js"
|
|
90
89
|
import { CONFIG_SCAFFOLD, renderBriefing } from "../install/index.js"
|
|
91
90
|
import { ToolCommands, type SelfGtd } from "./ToolCommands.js"
|
|
91
|
+
import {
|
|
92
|
+
SelectorUsageError,
|
|
93
|
+
writeSelection,
|
|
94
|
+
type ArtifactOut,
|
|
95
|
+
type CommandRequirements,
|
|
96
|
+
type JsonMode,
|
|
97
|
+
} from "./Output.js"
|
|
98
|
+
export {
|
|
99
|
+
SelectorUsageError,
|
|
100
|
+
type ArtifactOut,
|
|
101
|
+
type CommandRequirements,
|
|
102
|
+
type JsonMode,
|
|
103
|
+
} from "./Output.js"
|
|
104
|
+
import { runHistoryFetchCommand, runHistoryListCommand, runHistoryShowCommand } from "./History.js"
|
|
92
105
|
|
|
93
106
|
/**
|
|
94
107
|
* `Edge.ts`'s `Rest` is module-private (`.gtd/packages/05-step-core.md`
|
|
@@ -98,35 +111,6 @@ import { ToolCommands, type SelfGtd } from "./ToolCommands.js"
|
|
|
98
111
|
*/
|
|
99
112
|
type Rest = Effect.Effect.Success<typeof currentRest>
|
|
100
113
|
|
|
101
|
-
/**
|
|
102
|
-
* A command-level failure that should exit like a CLI usage error (2), not a
|
|
103
|
-
* generic runtime failure (1): an unknown `--json=<path>` selector, or a
|
|
104
|
-
* `gtd judge answer` verdict that fails to decode — malformed JSON on stdin,
|
|
105
|
-
* or one naming a question id the pending judgment never asked. Both are
|
|
106
|
-
* caller-input errors per `docs/cli.md`'s exit-code table, not a refusal
|
|
107
|
-
* about the resolved rest's own state.
|
|
108
|
-
*/
|
|
109
|
-
export class SelectorUsageError extends Error {}
|
|
110
|
-
|
|
111
|
-
/** The `--json=<path>` select branch, shared by all three emitting commands so none drift on the unknown-selector message. `absent` writes nothing and lets the caller's success path continue. */
|
|
112
|
-
const writeSelection = (
|
|
113
|
-
out: ArtifactOut,
|
|
114
|
-
fields: unknown,
|
|
115
|
-
path: string,
|
|
116
|
-
): Effect.Effect<void, Error> =>
|
|
117
|
-
Effect.gen(function* () {
|
|
118
|
-
const selection = selectPath(fields, path)
|
|
119
|
-
if (selection.kind === "value") {
|
|
120
|
-
out.write(`${selection.text}\n`)
|
|
121
|
-
} else if (selection.kind === "unknown") {
|
|
122
|
-
return yield* Effect.fail(
|
|
123
|
-
new SelectorUsageError(
|
|
124
|
-
`gtd: unknown --json selector "${selection.path}" — see \`gtd --help\``,
|
|
125
|
-
),
|
|
126
|
-
)
|
|
127
|
-
}
|
|
128
|
-
})
|
|
129
|
-
|
|
130
114
|
export type Command =
|
|
131
115
|
| { readonly kind: "lsp" }
|
|
132
116
|
| {
|
|
@@ -174,6 +158,9 @@ export type Command =
|
|
|
174
158
|
| { readonly kind: "base" }
|
|
175
159
|
| { readonly kind: "judge" }
|
|
176
160
|
| { readonly kind: "judgeAnswer" }
|
|
161
|
+
| { readonly kind: "historyList" }
|
|
162
|
+
| { readonly kind: "historyShow"; readonly ref: string }
|
|
163
|
+
| { readonly kind: "historyFetch"; readonly remote: string }
|
|
177
164
|
| {
|
|
178
165
|
readonly kind: "judgeRun"
|
|
179
166
|
readonly provider?: "fixed" | "jev" | "llm"
|
|
@@ -181,15 +168,6 @@ export type Command =
|
|
|
181
168
|
readonly model?: string
|
|
182
169
|
}
|
|
183
170
|
|
|
184
|
-
/**
|
|
185
|
-
* `--json`'s three shapes: absent, bare (the whole document), or `--json=<path>`
|
|
186
|
-
* (one reduced value). See `src/wire/Select.ts` for how `select` is later resolved.
|
|
187
|
-
*/
|
|
188
|
-
export type JsonMode =
|
|
189
|
-
| { readonly kind: "off" }
|
|
190
|
-
| { readonly kind: "document" }
|
|
191
|
-
| { readonly kind: "select"; readonly path: string }
|
|
192
|
-
|
|
193
171
|
/**
|
|
194
172
|
* What a command kind needs before it may run. `pure`/`removed` never reach
|
|
195
173
|
* `io.layers()` at all (they resolve to `output`/`usage` plans). `state`
|
|
@@ -199,31 +177,6 @@ export type JsonMode =
|
|
|
199
177
|
*/
|
|
200
178
|
export type Needs = "pure" | "removed" | "none" | "fs" | "state"
|
|
201
179
|
|
|
202
|
-
/**
|
|
203
|
-
* The all-or-nothing stdout buffer every `run*Command` handler writes
|
|
204
|
-
* through instead of `io.stdout` directly. `flush()` is called exactly once,
|
|
205
|
-
* by `runCli`, after the command's Effect succeeds; on any failure `flush()`
|
|
206
|
-
* is never reached, so the buffer is simply discarded and nothing reaches
|
|
207
|
-
* `io.stdout`. `ui` is the one handler that calls `flush()` itself,
|
|
208
|
-
* ahead of blocking on `Effect.never` — since it blocks forever, `runCli`'s
|
|
209
|
-
* own flush-on-success would otherwise never fire and its output would never
|
|
210
|
-
* reach stdout.
|
|
211
|
-
*/
|
|
212
|
-
export interface ArtifactOut {
|
|
213
|
-
readonly write: (chunk: string) => void
|
|
214
|
-
readonly flush: () => void
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export type CommandRequirements =
|
|
218
|
-
| GitService
|
|
219
|
-
| ConfigService
|
|
220
|
-
| ConfigDiscovery
|
|
221
|
-
| Workspace
|
|
222
|
-
| Host
|
|
223
|
-
| Narrator
|
|
224
|
-
| CommandRunner
|
|
225
|
-
| ToolCommands
|
|
226
|
-
|
|
227
180
|
const selfGtd = (): SelfGtd => {
|
|
228
181
|
const script = process.argv[1]
|
|
229
182
|
return script === undefined ? ["gtd"] : [process.execPath, script]
|
|
@@ -898,13 +851,6 @@ const runValidateCommand = (out: ArtifactOut): Effect.Effect<void, Error, Comman
|
|
|
898
851
|
)
|
|
899
852
|
})
|
|
900
853
|
|
|
901
|
-
/** `file:line:col: message` — the shape editors and grep already jump on. Stored 0-based, printed 1-based. A finding with a `line` but no `range`, or none at all, degrades to the shorter forms. */
|
|
902
|
-
export const formatFinding = (file: string, finding: SteeringFinding): string => {
|
|
903
|
-
if (finding.line === undefined) return finding.message
|
|
904
|
-
const col = finding.range !== undefined ? `:${finding.range.start.character + 1}` : ""
|
|
905
|
-
return `${file}:${finding.line + 1}${col}: ${finding.message}`
|
|
906
|
-
}
|
|
907
|
-
|
|
908
854
|
/**
|
|
909
855
|
* Runs `<mode>`'s parser over `<file>`, exiting non-zero on any finding.
|
|
910
856
|
* Resolves no workflow state and reads no config — this is what a seeded
|
|
@@ -1289,6 +1235,12 @@ const dispatchVoidCommand = (
|
|
|
1289
1235
|
return runJudgeAnswerCommand(json, out)
|
|
1290
1236
|
case "judgeRun":
|
|
1291
1237
|
return runJudgeRunCommand(command, out)
|
|
1238
|
+
case "historyList":
|
|
1239
|
+
return runHistoryListCommand(json, out)
|
|
1240
|
+
case "historyShow":
|
|
1241
|
+
return runHistoryShowCommand(command.ref, json, out)
|
|
1242
|
+
case "historyFetch":
|
|
1243
|
+
return runHistoryFetchCommand(command.remote, out)
|
|
1292
1244
|
}
|
|
1293
1245
|
}
|
|
1294
1246
|
|
package/src/edge/Edge.test.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { Effect, Exit } from "effect"
|
|
1
|
+
import { Effect, Exit, Option } from "effect"
|
|
2
2
|
import { describe, expect, it } from "vitest"
|
|
3
3
|
import { runCli } from "../cli/index.js"
|
|
4
4
|
import {
|
|
5
5
|
currentRest,
|
|
6
|
+
closedRunAt,
|
|
6
7
|
currentRun,
|
|
8
|
+
historyTrail,
|
|
7
9
|
noProcessUnderway,
|
|
8
10
|
renderRest,
|
|
9
11
|
restAt,
|
|
@@ -16,8 +18,11 @@ import {
|
|
|
16
18
|
UNATTRIBUTED_MODEL,
|
|
17
19
|
type RestRequirements,
|
|
18
20
|
} from "./Edge.js"
|
|
21
|
+
import { GTD_VERSION } from "../install/index.js"
|
|
22
|
+
import { processHistoryRef } from "../platform/index.js"
|
|
19
23
|
import { GitService, Workspace, type GitOperations, type WorkspaceOps } from "../platform/index.js"
|
|
20
24
|
import { formatCommitMessage, type CommitSpec } from "../replay/index.js"
|
|
25
|
+
import { contentHash } from "../workflow/index.js"
|
|
21
26
|
import {
|
|
22
27
|
fakeGitOperations,
|
|
23
28
|
InMemRepo,
|
|
@@ -197,7 +202,7 @@ describe("currentRun", () => {
|
|
|
197
202
|
const before = headOf(repo)
|
|
198
203
|
const opening = await enter(repo, "side")
|
|
199
204
|
expect(repo.lastCommitMessage()).toBe(
|
|
200
|
-
|
|
205
|
+
`gtd(human): fixing\n\nGtd-Workflow: side\nGtd-Var: base=\nGtd-Var: reviewer=nobody\nGtd-Config: ${contentHash(LINEAR)} gtd.config.ts\nGtd-Format: 1`,
|
|
201
206
|
)
|
|
202
207
|
const fixed = await land(repo, { "fix.txt": "x\n" })
|
|
203
208
|
const run = await provide(currentRun, repo)
|
|
@@ -229,7 +234,9 @@ export const again = async () => {
|
|
|
229
234
|
const repo = repoWith(STARTS_AT_INITIAL)
|
|
230
235
|
const before = headOf(repo)
|
|
231
236
|
const opening = await enter(repo, "again")
|
|
232
|
-
expect(repo.lastCommitMessage()).toBe(
|
|
237
|
+
expect(repo.lastCommitMessage()).toBe(
|
|
238
|
+
`gtd(human): idle\n\nGtd-Workflow: again\nGtd-Config: ${contentHash(STARTS_AT_INITIAL)} gtd.config.ts\nGtd-Format: 1`,
|
|
239
|
+
)
|
|
233
240
|
const fixed = await land(repo, { "fix.txt": "x\n" })
|
|
234
241
|
const run = await provide(currentRun, repo)
|
|
235
242
|
expect(run.workflow).toBe("again")
|
|
@@ -1014,3 +1021,254 @@ describe("the pending working tree is read at most once per restAt, and never st
|
|
|
1014
1021
|
expect(calls()).toBe(2)
|
|
1015
1022
|
})
|
|
1016
1023
|
})
|
|
1024
|
+
|
|
1025
|
+
describe("closedRunAt", () => {
|
|
1026
|
+
it("is the run summaryRun reads when HEAD is the tip", async () => {
|
|
1027
|
+
const repo = repoWith(LINEAR)
|
|
1028
|
+
await land(repo, { "a.txt": "a\n" })
|
|
1029
|
+
await land(repo, { "b.txt": "b\n" })
|
|
1030
|
+
const tip = await land(repo, { "c.txt": "c\n" })
|
|
1031
|
+
expect(await provide(closedRunAt(tip), repo)).toEqual(await provide(summaryRun, repo))
|
|
1032
|
+
})
|
|
1033
|
+
|
|
1034
|
+
it("resolves the closed run with HEAD elsewhere", async () => {
|
|
1035
|
+
const repo = repoWith(LINEAR)
|
|
1036
|
+
const building = await land(repo, { "a.txt": "a\n" })
|
|
1037
|
+
await land(repo, { "b.txt": "b\n" })
|
|
1038
|
+
const tip = await land(repo, { "c.txt": "c\n" })
|
|
1039
|
+
const closed = await provide(closedRunAt(tip), repo)
|
|
1040
|
+
repo.writeFile("d.txt", "d\n")
|
|
1041
|
+
repo.commitAllWithPrefix("chore: unrelated")
|
|
1042
|
+
const later = await provide(closedRunAt(tip), repo)
|
|
1043
|
+
expect(later).toEqual(closed)
|
|
1044
|
+
expect(later.closingHash).toBe(tip)
|
|
1045
|
+
expect(later.startHash).toBe(building)
|
|
1046
|
+
})
|
|
1047
|
+
})
|
|
1048
|
+
|
|
1049
|
+
describe("finishing landings keep history", () => {
|
|
1050
|
+
const landingOf = async (repo: InMemRepo, git?: GitOperations) => {
|
|
1051
|
+
const rest = await provide(currentRest, repo)
|
|
1052
|
+
const eff = snapshotFromRest(rest)
|
|
1053
|
+
const snapshot = await (git === undefined ? provide(eff, repo) : provideWithGit(eff, git, repo))
|
|
1054
|
+
const landing = snapshot.landing
|
|
1055
|
+
if (landing.kind !== "commit") throw new Error(`expected commit, got ${landing.kind}`)
|
|
1056
|
+
return landing
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
it("flow return: keeps refs/gtd/history/<startHash>, records version and branch", async () => {
|
|
1060
|
+
const repo = repoWith(LINEAR)
|
|
1061
|
+
const building = await land(repo, { "a.txt": "a\n" })
|
|
1062
|
+
await land(repo, { "b.txt": "b\n" })
|
|
1063
|
+
repo.writeFile("c.txt", "c\n")
|
|
1064
|
+
const landing = await landingOf(repo)
|
|
1065
|
+
expect(landing.to).toBe("idle")
|
|
1066
|
+
expect(landing.keepHistory).toBe(processHistoryRef(building))
|
|
1067
|
+
expect(landing.spec.version).toBe(GTD_VERSION)
|
|
1068
|
+
expect(landing.spec.branch).toBe(repo.branchName())
|
|
1069
|
+
})
|
|
1070
|
+
|
|
1071
|
+
it("restart() is a finish too", async () => {
|
|
1072
|
+
const repo = repoWith(`import { agent, human, restart } from "@pmelab/gtd/flows"
|
|
1073
|
+
|
|
1074
|
+
export default async () => {
|
|
1075
|
+
await human("idle")
|
|
1076
|
+
await agent("building", "build-prompt")
|
|
1077
|
+
await restart()
|
|
1078
|
+
}
|
|
1079
|
+
`)
|
|
1080
|
+
const building = await land(repo, { "a.txt": "a\n" })
|
|
1081
|
+
repo.writeFile("b.txt", "b\n")
|
|
1082
|
+
const landing = await landingOf(repo)
|
|
1083
|
+
expect(landing.to).toBe("idle")
|
|
1084
|
+
expect(landing.keepHistory).toBe(processHistoryRef(building))
|
|
1085
|
+
})
|
|
1086
|
+
|
|
1087
|
+
it("starting landings and mid-process landings carry no keepHistory and no new trailers", async () => {
|
|
1088
|
+
const repo = repoWith(LINEAR)
|
|
1089
|
+
repo.writeFile("a.txt", "a\n")
|
|
1090
|
+
const starting = await landingOf(repo)
|
|
1091
|
+
expect(starting.to).toBe("building")
|
|
1092
|
+
expect(starting.keepHistory).toBeUndefined()
|
|
1093
|
+
expect(starting.spec.version).toBeUndefined()
|
|
1094
|
+
expect(starting.spec.branch).toBeUndefined()
|
|
1095
|
+
await land(repo, { "a.txt": "a\n" })
|
|
1096
|
+
repo.writeFile("b.txt", "b\n")
|
|
1097
|
+
const mid = await landingOf(repo)
|
|
1098
|
+
expect(mid.to).toBe("checking")
|
|
1099
|
+
expect(mid.keepHistory).toBeUndefined()
|
|
1100
|
+
expect(mid.spec.version).toBeUndefined()
|
|
1101
|
+
expect(mid.spec.branch).toBeUndefined()
|
|
1102
|
+
})
|
|
1103
|
+
|
|
1104
|
+
it("detached HEAD keeps history but records no branch", async () => {
|
|
1105
|
+
const repo = repoWith(LINEAR)
|
|
1106
|
+
const building = await land(repo, { "a.txt": "a\n" })
|
|
1107
|
+
await land(repo, { "b.txt": "b\n" })
|
|
1108
|
+
repo.writeFile("c.txt", "c\n")
|
|
1109
|
+
const detached: GitOperations = {
|
|
1110
|
+
...fakeGitOperations(repo),
|
|
1111
|
+
currentBranch: () => Effect.succeed(Option.none()),
|
|
1112
|
+
}
|
|
1113
|
+
const landing = await landingOf(repo, detached)
|
|
1114
|
+
expect(landing.keepHistory).toBe(processHistoryRef(building))
|
|
1115
|
+
expect(landing.spec.version).toBe(GTD_VERSION)
|
|
1116
|
+
expect(landing.spec.branch).toBeUndefined()
|
|
1117
|
+
})
|
|
1118
|
+
|
|
1119
|
+
it("a --workflow process's entry is its opening commit", async () => {
|
|
1120
|
+
const repo = repoWith(`import { agent, human } from "@pmelab/gtd/flows"
|
|
1121
|
+
|
|
1122
|
+
export default async () => {
|
|
1123
|
+
await human("idle")
|
|
1124
|
+
await agent("building", "build-prompt")
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
export const side = async () => {
|
|
1128
|
+
await agent("fixing", "fix-prompt")
|
|
1129
|
+
}
|
|
1130
|
+
`)
|
|
1131
|
+
const opening = await enter(repo, "side")
|
|
1132
|
+
repo.writeFile("a.txt", "a\n")
|
|
1133
|
+
const landing = await landingOf(repo)
|
|
1134
|
+
expect(landing.to).toBe("idle")
|
|
1135
|
+
expect(landing.keepHistory).toBe(processHistoryRef(opening))
|
|
1136
|
+
})
|
|
1137
|
+
|
|
1138
|
+
it("the landing script overwrites an existing ref with its own commit", async () => {
|
|
1139
|
+
const repo = repoWith(LINEAR)
|
|
1140
|
+
const building = await land(repo, { "a.txt": "a\n" })
|
|
1141
|
+
await land(repo, { "b.txt": "b\n" })
|
|
1142
|
+
const ref = processHistoryRef(building)
|
|
1143
|
+
repo.updateRef(ref, building)
|
|
1144
|
+
const tip = await land(repo, { "c.txt": "c\n" })
|
|
1145
|
+
expect(repo.resolveRef(ref)).toBe(tip)
|
|
1146
|
+
})
|
|
1147
|
+
})
|
|
1148
|
+
|
|
1149
|
+
describe("historyTrail", () => {
|
|
1150
|
+
const finished = async (config = LINEAR) => {
|
|
1151
|
+
const repo = repoWith(config)
|
|
1152
|
+
const building = await land(repo, { "a.txt": "a\n" })
|
|
1153
|
+
const checking = await land(repo, { "b.txt": "b\n" })
|
|
1154
|
+
const tip = await land(repo, { "c.txt": "c\n" })
|
|
1155
|
+
return { repo, building, checking, tip }
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
it("replays every commit of the closed run, with HEAD elsewhere", async () => {
|
|
1159
|
+
const { repo, building, checking, tip } = await finished()
|
|
1160
|
+
repo.writeFile("d.txt", "d\n")
|
|
1161
|
+
repo.commitAllWithPrefix("chore: unrelated")
|
|
1162
|
+
const trail = await provide(historyTrail(tip), repo)
|
|
1163
|
+
expect(trail.tip).toBe(tip)
|
|
1164
|
+
expect(trail.ended).toBe("return")
|
|
1165
|
+
expect(trail.steps.map((s) => s.hash)).toEqual([building, checking, tip])
|
|
1166
|
+
expect(trail.steps[2]).toMatchObject({
|
|
1167
|
+
subject: "gtd(agent): checking → idle",
|
|
1168
|
+
actor: "agent",
|
|
1169
|
+
from: "checking",
|
|
1170
|
+
to: "idle",
|
|
1171
|
+
verdicts: [],
|
|
1172
|
+
})
|
|
1173
|
+
expect(trail.steps[0]).toMatchObject({ actor: "human", to: "building" })
|
|
1174
|
+
})
|
|
1175
|
+
|
|
1176
|
+
it("a restart() ends the trail as restart", async () => {
|
|
1177
|
+
const repo = repoWith(`import { agent, human, restart } from "@pmelab/gtd/flows"
|
|
1178
|
+
|
|
1179
|
+
export default async () => {
|
|
1180
|
+
await human("idle")
|
|
1181
|
+
await agent("building", "build-prompt")
|
|
1182
|
+
await restart()
|
|
1183
|
+
}
|
|
1184
|
+
`)
|
|
1185
|
+
await land(repo, { "a.txt": "a\n" })
|
|
1186
|
+
const tip = await land(repo, { "b.txt": "b\n" })
|
|
1187
|
+
expect((await provide(historyTrail(tip), repo)).ended).toBe("restart")
|
|
1188
|
+
})
|
|
1189
|
+
|
|
1190
|
+
it("carries each judge verdict on the commit that recorded it", async () => {
|
|
1191
|
+
const repo = repoWith(LINEAR)
|
|
1192
|
+
await land(repo, { "a.txt": "a\n" })
|
|
1193
|
+
const verdictCommit = commit(
|
|
1194
|
+
repo,
|
|
1195
|
+
{
|
|
1196
|
+
actor: "agent",
|
|
1197
|
+
from: "building",
|
|
1198
|
+
to: "checking",
|
|
1199
|
+
step: { name: "building", occurrence: 1 },
|
|
1200
|
+
judge: [{ id: "q", answer: "yes", p: 0.9 }],
|
|
1201
|
+
},
|
|
1202
|
+
{ "b.txt": "b\n" },
|
|
1203
|
+
)
|
|
1204
|
+
const tip = await land(repo, { "c.txt": "c\n" })
|
|
1205
|
+
const trail = await provide(historyTrail(tip), repo)
|
|
1206
|
+
expect(trail.steps.find((s) => s.hash === verdictCommit)?.verdicts).toEqual([
|
|
1207
|
+
{ id: "q", answer: "yes", p: 0.9 },
|
|
1208
|
+
])
|
|
1209
|
+
expect(trail.steps.filter((s) => s.verdicts.length > 0)).toHaveLength(1)
|
|
1210
|
+
})
|
|
1211
|
+
|
|
1212
|
+
it("records the version and warns about none while they match", async () => {
|
|
1213
|
+
const { repo, tip } = await finished()
|
|
1214
|
+
const trail = await provide(historyTrail(tip), repo)
|
|
1215
|
+
expect(trail.version).toEqual({ recorded: GTD_VERSION, running: GTD_VERSION })
|
|
1216
|
+
expect(trail.versionWarning).toBeUndefined()
|
|
1217
|
+
})
|
|
1218
|
+
|
|
1219
|
+
const withTipMessage = (repo: InMemRepo, edit: (message: string) => string): GitOperations => {
|
|
1220
|
+
const real = fakeGitOperations(repo)
|
|
1221
|
+
return {
|
|
1222
|
+
...real,
|
|
1223
|
+
lastCommitMessage: (ref) => Effect.map(real.lastCommitMessage(ref), edit),
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
it("warns, never refuses, when the recorded version differs", async () => {
|
|
1228
|
+
const { repo, tip } = await finished()
|
|
1229
|
+
const git = withTipMessage(repo, (m) =>
|
|
1230
|
+
m.replace(`Gtd-Version: ${GTD_VERSION}`, "Gtd-Version: 0.0.1"),
|
|
1231
|
+
)
|
|
1232
|
+
const trail = await Effect.runPromise(
|
|
1233
|
+
historyTrail(tip).pipe(
|
|
1234
|
+
Effect.provideService(GitService, git),
|
|
1235
|
+
Effect.provide(testLayers(repo, {})),
|
|
1236
|
+
),
|
|
1237
|
+
)
|
|
1238
|
+
expect(trail.version.recorded).toBe("0.0.1")
|
|
1239
|
+
expect(trail.versionWarning).toContain("0.0.1")
|
|
1240
|
+
expect(trail.versionWarning).toContain(GTD_VERSION)
|
|
1241
|
+
})
|
|
1242
|
+
|
|
1243
|
+
it("a missing recorded version counts as different", async () => {
|
|
1244
|
+
const { repo, tip } = await finished()
|
|
1245
|
+
const git = withTipMessage(repo, (m) => m.replace(/Gtd-Version: .*\n?/, ""))
|
|
1246
|
+
const trail = await Effect.runPromise(
|
|
1247
|
+
historyTrail(tip).pipe(
|
|
1248
|
+
Effect.provideService(GitService, git),
|
|
1249
|
+
Effect.provide(testLayers(repo, {})),
|
|
1250
|
+
),
|
|
1251
|
+
)
|
|
1252
|
+
expect(trail.version.recorded).toBeUndefined()
|
|
1253
|
+
expect(trail.versionWarning).toContain(GTD_VERSION)
|
|
1254
|
+
})
|
|
1255
|
+
|
|
1256
|
+
it("refuses naming the config file that differs at the tip", async () => {
|
|
1257
|
+
const { repo, tip } = await finished()
|
|
1258
|
+
repo.writeFile("gtd.config.ts", `${LINEAR}\n// edited\n`)
|
|
1259
|
+
repo.commitAllWithPrefix("chore: edit config")
|
|
1260
|
+
const exit = await provideExit(historyTrail(tip), repo)
|
|
1261
|
+
expect(Exit.isFailure(exit)).toBe(true)
|
|
1262
|
+
expect(String(Exit.isFailure(exit) && exit.cause)).toContain("gtd.config.ts")
|
|
1263
|
+
})
|
|
1264
|
+
|
|
1265
|
+
it("refuses naming a config file missing from the tip", async () => {
|
|
1266
|
+
const { repo, tip } = await finished()
|
|
1267
|
+
repo.writeFile("gtd.config.ts", `import "./extra.ts"\n${LINEAR}`)
|
|
1268
|
+
repo.writeFile("extra.ts", "export {}\n")
|
|
1269
|
+
repo.commitAllWithPrefix("chore: split config")
|
|
1270
|
+
const exit = await provideExit(historyTrail(tip), repo)
|
|
1271
|
+
expect(Exit.isFailure(exit)).toBe(true)
|
|
1272
|
+
expect(String(Exit.isFailure(exit) && exit.cause)).toContain("gtd.config.ts")
|
|
1273
|
+
})
|
|
1274
|
+
})
|