@pmelab/gtd 60.1.0 → 62.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 +16 -0
- package/dist/chunks/{testkit-BhTey1Do.mjs → testkit-XQeYCNMn.mjs} +1670 -1470
- package/dist/chunks/{wire-DZ8R1RU9.mjs → wire-CPNSm_9N.mjs} +27 -12
- package/dist/gtd.bundle.mjs +991 -627
- package/dist/gtd.core.mjs +117 -117
- package/dist/testkit/gherkin.mjs +1 -1
- package/dist/testkit.mjs +1 -1
- package/dist/wire.mjs +2 -2
- package/package.json +1 -1
- package/src/cli/Cli.ts +6 -1
- package/src/cli/History.test.ts +10 -1
- package/src/cli/Launch.test.ts +52 -0
- package/src/cli/Launch.ts +143 -0
- package/src/cli/Metrics.test.ts +10 -1
- package/src/cli/Program.test.ts +30 -20
- package/src/cli/Program.ts +4 -2
- package/src/cli/Strays.ts +3 -2
- package/src/cli/index.ts +1 -0
- package/src/edge/Edge.test.ts +79 -26
- package/src/edge/Edge.ts +178 -99
- package/src/emit/GitScript.ts +21 -3
- package/src/emit/index.ts +3 -0
- 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/platform/PinFile.ts +61 -0
- package/src/platform/index.ts +1 -0
- 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 +11 -48
- package/src/replay/Trailers.ts +36 -42
- 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 -3
- package/src/step/planStart.ts +1 -5
- 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/GitTiers.ts +23 -0
- package/src/twins/InMemRepo.ts +14 -0
- package/src/twins/PinBeats.ts +33 -0
- package/src/twins/PinFile.test.ts +107 -0
- package/src/twins/PinFile.ts +12 -0
- package/src/twins/index.ts +2 -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/fingerprint.ts +2 -2
- package/src/workflow/index.ts +2 -2
- package/src/workflow/load.ts +14 -1
- package/src/workflow/pin.ts +15 -7
package/src/edge/Edge.test.ts
CHANGED
|
@@ -23,13 +23,14 @@ import { keptRef, processHistoryRef } from "../platform/index.js"
|
|
|
23
23
|
import { GitService, Workspace, type GitOperations, type WorkspaceOps } from "../platform/index.js"
|
|
24
24
|
import type { Mark } from "../flows/index.js"
|
|
25
25
|
import { formatCommitMessage, type CommitSpec } from "../replay/index.js"
|
|
26
|
-
import {
|
|
26
|
+
import { StartFlags } from "../workflow/index.js"
|
|
27
27
|
import {
|
|
28
28
|
fakeGitOperations,
|
|
29
29
|
InMemRepo,
|
|
30
30
|
applyEmittedScript,
|
|
31
31
|
makeCapturingCliIo,
|
|
32
32
|
makeInMemoryWorkspaceOps,
|
|
33
|
+
takePinBeat,
|
|
33
34
|
testLayers,
|
|
34
35
|
} from "../twins/index.js"
|
|
35
36
|
|
|
@@ -74,7 +75,7 @@ const write = (repo: InMemRepo, files: Files): void => {
|
|
|
74
75
|
for (const [path, content] of Object.entries(files)) repo.writeFile(path, content)
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
const headOf = (repo: InMemRepo): string => repo.resolveRef(
|
|
78
|
+
const headOf = (repo: InMemRepo, ref = "HEAD"): string => repo.resolveRef(ref)!
|
|
78
79
|
|
|
79
80
|
const repoWith = (config: string, files: Files = {}): InMemRepo => {
|
|
80
81
|
const repo = new InMemRepo()
|
|
@@ -95,19 +96,27 @@ const applied = (repo: InMemRepo, script: string): string => {
|
|
|
95
96
|
return headOf(repo)
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* Lands the pending turn the way a driver does — `gtd land --json`, then its
|
|
101
|
+
* script, then the pin or unpin beat it leads to — and returns the turn's
|
|
102
|
+
* commit, or the closing commit when the turn led to the unpin.
|
|
103
|
+
*/
|
|
99
104
|
const land = async (repo: InMemRepo, files: Files = {}): Promise<string> => {
|
|
100
105
|
write(repo, files)
|
|
101
106
|
const { stdout, stderr, exitCode } = await cli(repo, "land", "--json")
|
|
102
107
|
if (exitCode !== 0) throw new Error(`gtd land exited ${exitCode}: ${stderr}${stdout}`)
|
|
103
|
-
|
|
108
|
+
const turn = applied(repo, (JSON.parse(stdout) as { readonly script: string }).script)
|
|
109
|
+
await takePinBeat(repo, "add")
|
|
110
|
+
return (await takePinBeat(repo, "remove")) ? headOf(repo) : turn
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
const enter = async (repo: InMemRepo, workflow: string, ...vars: string[]): Promise<string> => {
|
|
107
114
|
const flags = vars.flatMap((v) => ["--var", v])
|
|
108
115
|
const { stdout, stderr, exitCode } = await cli(repo, "start", workflow, ...flags)
|
|
109
116
|
if (exitCode !== 0) throw new Error(`gtd start exited ${exitCode}: ${stderr}`)
|
|
110
|
-
|
|
117
|
+
const opening = applied(repo, stdout)
|
|
118
|
+
await takePinBeat(repo, "add")
|
|
119
|
+
return opening
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
/** Commits an exact message — for trailers no landing writes on its own. */
|
|
@@ -174,7 +183,11 @@ describe("currentRun", () => {
|
|
|
174
183
|
{ state: "checking", hash: checking, actor: "agent" },
|
|
175
184
|
])
|
|
176
185
|
expect(run.episode.base).toBe(boundary)
|
|
177
|
-
expect(run.episode.commits.map((c) => c.
|
|
186
|
+
expect(run.episode.commits.map((c) => c.message.split("\n")[0])).toEqual([
|
|
187
|
+
"gtd(human): idle → building",
|
|
188
|
+
"gtd(check): pin → building",
|
|
189
|
+
"gtd(agent): building → checking",
|
|
190
|
+
])
|
|
178
191
|
})
|
|
179
192
|
|
|
180
193
|
it("a non-gtd commit on top of a process bounds a fresh episode", async () => {
|
|
@@ -192,7 +205,7 @@ describe("currentRun", () => {
|
|
|
192
205
|
await land(repo, { "a.txt": "a\n" })
|
|
193
206
|
await land(repo, { "b.txt": "b\n" })
|
|
194
207
|
const closing = await land(repo, { "c.txt": "c\n" })
|
|
195
|
-
expect(repo.lastCommitSubject()).toBe("gtd(
|
|
208
|
+
expect(repo.lastCommitSubject()).toBe("gtd(check): unpin → idle")
|
|
196
209
|
const next = await land(repo, { "d.txt": "d\n" })
|
|
197
210
|
const run = await provide(currentRun, repo)
|
|
198
211
|
expect(run.startParentHash).toBe(closing)
|
|
@@ -203,8 +216,8 @@ describe("currentRun", () => {
|
|
|
203
216
|
const repo = repoWith(LINEAR)
|
|
204
217
|
const before = headOf(repo)
|
|
205
218
|
const opening = await enter(repo, "side")
|
|
206
|
-
expect(repo.lastCommitMessage()).toBe(
|
|
207
|
-
`gtd(human): fixing\n\nGtd-Workflow: side\nGtd-Var: base=\nGtd-Var: reviewer=nobody\nGtd-
|
|
219
|
+
expect(repo.lastCommitMessage(opening)).toBe(
|
|
220
|
+
`gtd(human): fixing\n\nGtd-Workflow: side\nGtd-Var: base=\nGtd-Var: reviewer=nobody\nGtd-Format: 2`,
|
|
208
221
|
)
|
|
209
222
|
const fixed = await land(repo, { "fix.txt": "x\n" })
|
|
210
223
|
const run = await provide(currentRun, repo)
|
|
@@ -216,7 +229,7 @@ describe("currentRun", () => {
|
|
|
216
229
|
{ state: "tidying", hash: fixed, actor: "agent" },
|
|
217
230
|
])
|
|
218
231
|
expect(run.episode.base).toBe(opening)
|
|
219
|
-
expect(run.episode.commits.map((c) => c.hash)).toEqual([fixed])
|
|
232
|
+
expect(run.episode.commits.map((c) => c.hash)).toEqual([headOf(repo, `${fixed}~1`), fixed])
|
|
220
233
|
})
|
|
221
234
|
|
|
222
235
|
it("a workflow starting at the default's initial step still opens its own episode, not the empty tree", async () => {
|
|
@@ -236,8 +249,8 @@ export const again = workflow(async () => {
|
|
|
236
249
|
const repo = repoWith(STARTS_AT_INITIAL)
|
|
237
250
|
const before = headOf(repo)
|
|
238
251
|
const opening = await enter(repo, "again")
|
|
239
|
-
expect(repo.lastCommitMessage()).toBe(
|
|
240
|
-
`gtd(human): idle\n\nGtd-Workflow: again\nGtd-
|
|
252
|
+
expect(repo.lastCommitMessage(opening)).toBe(
|
|
253
|
+
`gtd(human): idle\n\nGtd-Workflow: again\nGtd-Format: 2`,
|
|
241
254
|
)
|
|
242
255
|
const fixed = await land(repo, { "fix.txt": "x\n" })
|
|
243
256
|
const run = await provide(currentRun, repo)
|
|
@@ -374,15 +387,23 @@ export const again = workflow(async () => {
|
|
|
374
387
|
it("describes a step commit that changed something", async () => {
|
|
375
388
|
const repo = repoWith(LINEAR)
|
|
376
389
|
await land(repo, { "a.txt": "a\n" })
|
|
390
|
+
await land(repo, { "b.txt": "b\n" })
|
|
377
391
|
const run = await provide(currentRun, repo)
|
|
378
|
-
expect(run.headTurn).toEqual({ state: "
|
|
392
|
+
expect(run.headTurn).toEqual({ state: "checking", actor: "agent", empty: false, step: true })
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it("is undefined at a pin commit, which is no turn", async () => {
|
|
396
|
+
const repo = repoWith(LINEAR)
|
|
397
|
+
await land(repo, { "a.txt": "a\n" })
|
|
398
|
+
expect(repo.lastCommitSubject()).toBe("gtd(check): pin → building")
|
|
399
|
+
expect((await provide(currentRun, repo)).headTurn).toBeUndefined()
|
|
379
400
|
})
|
|
380
401
|
|
|
381
402
|
it("describes an attempt: a step-less, empty commit", async () => {
|
|
382
403
|
const repo = repoWith(LINEAR)
|
|
383
404
|
await land(repo, { "a.txt": "a\n" })
|
|
384
405
|
await land(repo)
|
|
385
|
-
expect(repo.lastCommitMessage()).toBe("gtd(agent): building\n\nGtd-Format:
|
|
406
|
+
expect(repo.lastCommitMessage()).toBe("gtd(agent): building\n\nGtd-Format: 2")
|
|
386
407
|
const run = await provide(currentRun, repo)
|
|
387
408
|
expect(run.headTurn).toEqual({ state: "building", actor: "agent", empty: true, step: false })
|
|
388
409
|
})
|
|
@@ -523,7 +544,8 @@ describe("summaryRun", () => {
|
|
|
523
544
|
expect(summary.trace).toEqual([
|
|
524
545
|
{ state: "building", hash: building, actor: "human" },
|
|
525
546
|
{ state: "checking", hash: checking, actor: "agent" },
|
|
526
|
-
{ state: "
|
|
547
|
+
{ state: "unpin", hash: headOf(repo, `${closing}~1`), actor: "agent" },
|
|
548
|
+
{ state: "idle", hash: closing, actor: "check" },
|
|
527
549
|
])
|
|
528
550
|
})
|
|
529
551
|
|
|
@@ -635,7 +657,9 @@ export default workflow(async () => {
|
|
|
635
657
|
`--model=${model}`,
|
|
636
658
|
)
|
|
637
659
|
if (exitCode !== 0) throw new Error(`gtd land exited ${exitCode}: ${stderr}${stdout}`)
|
|
638
|
-
|
|
660
|
+
const turn = applied(repo, (JSON.parse(stdout) as { readonly script: string }).script)
|
|
661
|
+
await takePinBeat(repo, "add")
|
|
662
|
+
return (await takePinBeat(repo, "remove")) ? headOf(repo) : turn
|
|
639
663
|
}
|
|
640
664
|
type Rendered = {
|
|
641
665
|
entryCommit: string
|
|
@@ -711,6 +735,7 @@ describe("restAt", () => {
|
|
|
711
735
|
)
|
|
712
736
|
expect(exitCode).toBe(0)
|
|
713
737
|
await applied(repo, (JSON.parse(stdout) as { readonly script: string }).script)
|
|
738
|
+
await takePinBeat(repo, "add")
|
|
714
739
|
}
|
|
715
740
|
const { cost } = await provide(currentRest, repo)
|
|
716
741
|
expect(cost.total).toBe(11)
|
|
@@ -806,6 +831,7 @@ export const review = workflow(async () => {
|
|
|
806
831
|
it("is the base the resting step names", async () => {
|
|
807
832
|
const repo = repoWith(LOOP)
|
|
808
833
|
const first = await land(repo, { "a.txt": "a\n" })
|
|
834
|
+
expect(repo.lastCommitSubject()).toBe("gtd(check): pin → checkpoint")
|
|
809
835
|
expect(reviewBaseFor(await provide(currentRest, repo))).toBe(first)
|
|
810
836
|
await land(repo, { "b.txt": "b\n" })
|
|
811
837
|
expect(reviewBaseFor(await provide(currentRest, repo))).toBe(first)
|
|
@@ -1043,8 +1069,8 @@ export default workflow(async () => {
|
|
|
1043
1069
|
|
|
1044
1070
|
it("an ordinary start's first commit pins every process setting, sorted by name", async () => {
|
|
1045
1071
|
const repo = repoWith(LINEAR)
|
|
1046
|
-
await land(repo, { "a.txt": "a\n" })
|
|
1047
|
-
expect(repo.lastCommitMessage()).toContain(
|
|
1072
|
+
const start = await land(repo, { "a.txt": "a\n" })
|
|
1073
|
+
expect(repo.lastCommitMessage(start)).toContain(
|
|
1048
1074
|
"\n\nGtd-Step: idle#1\nGtd-Var: base=\nGtd-Var: reviewer=nobody",
|
|
1049
1075
|
)
|
|
1050
1076
|
await land(repo)
|
|
@@ -1265,13 +1291,34 @@ describe("finishing landings keep history", () => {
|
|
|
1265
1291
|
return landing
|
|
1266
1292
|
}
|
|
1267
1293
|
|
|
1294
|
+
/** Lands the turn that ends the flow, and runs the unpin beat's script without landing it. */
|
|
1295
|
+
const toUnpin = async (repo: InMemRepo, files: Files) => {
|
|
1296
|
+
write(repo, files)
|
|
1297
|
+
const turn = await cli(repo, "land", "--json")
|
|
1298
|
+
applied(repo, (JSON.parse(turn.stdout) as { readonly script: string }).script)
|
|
1299
|
+
const beat = await cli(repo, "next", "--json")
|
|
1300
|
+
applied(repo, (JSON.parse(beat.stdout) as { readonly content: string }).content)
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
it("a pinned process's last turn lands at the unpin, keeping no history yet", async () => {
|
|
1304
|
+
const repo = repoWith(LINEAR)
|
|
1305
|
+
await land(repo, { "a.txt": "a\n" })
|
|
1306
|
+
await land(repo, { "b.txt": "b\n" })
|
|
1307
|
+
repo.writeFile("c.txt", "c\n")
|
|
1308
|
+
const landing = await landingOf(repo)
|
|
1309
|
+
expect(landing.to).toBe("unpin")
|
|
1310
|
+
expect(landing.keepHistory).toBeUndefined()
|
|
1311
|
+
expect(landing.spec.version).toBeUndefined()
|
|
1312
|
+
})
|
|
1313
|
+
|
|
1268
1314
|
it("flow return: keeps refs/gtd/history/<startHash>, records version and branch", async () => {
|
|
1269
1315
|
const repo = repoWith(LINEAR)
|
|
1270
1316
|
const building = await land(repo, { "a.txt": "a\n" })
|
|
1271
1317
|
await land(repo, { "b.txt": "b\n" })
|
|
1272
|
-
repo
|
|
1318
|
+
await toUnpin(repo, { "c.txt": "c\n" })
|
|
1273
1319
|
const landing = await landingOf(repo)
|
|
1274
1320
|
expect(landing.to).toBe("idle")
|
|
1321
|
+
expect(landing.spec.pin).toBe("remove")
|
|
1275
1322
|
expect(landing.keepHistory).toBe(processHistoryRef(building))
|
|
1276
1323
|
expect(landing.spec.version).toBe(GTD_VERSION)
|
|
1277
1324
|
expect(landing.spec.branch).toBe(repo.branchName())
|
|
@@ -1287,7 +1334,7 @@ export default async () => {
|
|
|
1287
1334
|
}
|
|
1288
1335
|
`)
|
|
1289
1336
|
const building = await land(repo, { "a.txt": "a\n" })
|
|
1290
|
-
repo
|
|
1337
|
+
await toUnpin(repo, { "b.txt": "b\n" })
|
|
1291
1338
|
const landing = await landingOf(repo)
|
|
1292
1339
|
expect(landing.to).toBe("idle")
|
|
1293
1340
|
expect(landing.keepHistory).toBe(processHistoryRef(building))
|
|
@@ -1314,7 +1361,7 @@ export default async () => {
|
|
|
1314
1361
|
const repo = repoWith(LINEAR)
|
|
1315
1362
|
const building = await land(repo, { "a.txt": "a\n" })
|
|
1316
1363
|
await land(repo, { "b.txt": "b\n" })
|
|
1317
|
-
repo
|
|
1364
|
+
await toUnpin(repo, { "c.txt": "c\n" })
|
|
1318
1365
|
const detached: GitOperations = {
|
|
1319
1366
|
...fakeGitOperations(repo),
|
|
1320
1367
|
currentBranch: () => Effect.succeed(Option.none()),
|
|
@@ -1338,7 +1385,7 @@ export const side = workflow(async () => {
|
|
|
1338
1385
|
})
|
|
1339
1386
|
`)
|
|
1340
1387
|
const opening = await enter(repo, "side")
|
|
1341
|
-
repo
|
|
1388
|
+
await toUnpin(repo, { "a.txt": "a\n" })
|
|
1342
1389
|
const landing = await landingOf(repo)
|
|
1343
1390
|
expect(landing.to).toBe("idle")
|
|
1344
1391
|
expect(landing.keepHistory).toBe(processHistoryRef(opening))
|
|
@@ -1468,14 +1515,20 @@ describe("historyTrail", () => {
|
|
|
1468
1515
|
const trail = await provide(historyTrail(tip), repo)
|
|
1469
1516
|
expect(trail.tip).toBe(tip)
|
|
1470
1517
|
expect(trail.ended).toBe("return")
|
|
1471
|
-
expect(trail.steps.map((s) => s.hash)).toEqual([
|
|
1518
|
+
expect(trail.steps.map((s) => s.hash)).toEqual([
|
|
1519
|
+
building,
|
|
1520
|
+
checking,
|
|
1521
|
+
headOf(repo, `${tip}~1`),
|
|
1522
|
+
tip,
|
|
1523
|
+
])
|
|
1472
1524
|
expect(trail.steps[2]).toMatchObject({
|
|
1473
|
-
subject: "gtd(agent): checking →
|
|
1525
|
+
subject: "gtd(agent): checking → unpin",
|
|
1474
1526
|
actor: "agent",
|
|
1475
1527
|
from: "checking",
|
|
1476
|
-
to: "
|
|
1528
|
+
to: "unpin",
|
|
1477
1529
|
verdicts: [],
|
|
1478
1530
|
})
|
|
1531
|
+
expect(trail.steps[3]).toMatchObject({ subject: "gtd(check): unpin → idle", actor: "check" })
|
|
1479
1532
|
expect(trail.steps[0]).toMatchObject({ actor: "human", to: "building" })
|
|
1480
1533
|
})
|
|
1481
1534
|
|
|
@@ -1628,7 +1681,7 @@ export default async () => {
|
|
|
1628
1681
|
)
|
|
1629
1682
|
}).pipe(Effect.provide(testLayers(repo, {}))),
|
|
1630
1683
|
)
|
|
1631
|
-
expect(trail.steps).toHaveLength(
|
|
1684
|
+
expect(trail.steps).toHaveLength(4)
|
|
1632
1685
|
expect(calls).toBe(1)
|
|
1633
1686
|
})
|
|
1634
1687
|
|