@pmelab/gtd 64.3.0 → 66.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 +35 -25
- package/claude/hooks/assist.ts +9 -35
- package/claude/hooks/doors.ts +4 -4
- package/claude/hooks/drive.ts +10 -1
- package/claude/hooks/git.ts +56 -0
- package/claude/hooks/handoff.ts +11 -11
- package/claude/hooks/jobs.ts +8 -8
- package/claude/hooks/pr.ts +86 -26
- package/claude/hooks/register.tsx +35 -82
- package/claude/hooks/tools.ts +7 -20
- package/claude/hooks/wording.ts +3 -3
- package/dist/chunks/{flows-dnIE_qbe.mjs → flows-CmQplvh1.mjs} +146 -35
- package/dist/chunks/{steering-DEh_MS40.mjs → steering-CMrePUiY.mjs} +115 -17
- package/dist/chunks/{testkit-BUX-47TD.mjs → testkit-BJhAAVGt.mjs} +905 -99
- package/dist/chunks/{wire-CaPJ9Tme.mjs → wire-xTbzFc26.mjs} +12 -6
- package/dist/flows.mjs +1 -1
- package/dist/gtd.bundle.mjs +965 -163
- package/dist/gtd.core.mjs +150 -134
- package/dist/steering.mjs +1 -1
- package/dist/testkit/gherkin.mjs +1 -1
- package/dist/testkit.mjs +2 -2
- package/dist/web/main.css +1 -1
- package/dist/web/main.js +185 -18
- package/dist/wire.mjs +1 -1
- package/dist/workflow-kit.mjs +9 -3
- package/package.json +1 -1
- package/src/cli/Cli.ts +5 -5
- package/src/cli/History.test.ts +10 -8
- package/src/cli/Metrics.test.ts +2 -1
- package/src/cli/Program.test.ts +4 -2
- package/src/cli/Program.ts +105 -7
- package/src/edge/Edge.test.ts +37 -16
- package/src/edge/Edge.ts +380 -33
- package/src/emit/GitScript.ts +116 -15
- package/src/emit/index.ts +7 -0
- package/src/flows/config.ts +7 -1
- package/src/flows/helpers.ts +36 -18
- package/src/flows/index.test.ts +4 -0
- package/src/flows/index.ts +7 -0
- package/src/flows/runtime.ts +153 -15
- package/src/install/Install.ts +9 -2
- package/src/platform/Git.test.ts +7 -11
- package/src/platform/NodeSync.ts +2 -0
- package/src/platform/RetainedHistory.ts +3 -0
- package/src/platform/SyncIo.ts +2 -0
- package/src/platform/Workspace.test.ts +37 -0
- package/src/platform/Workspace.ts +36 -0
- package/src/platform/index.ts +1 -0
- package/src/replay/Replay.test.ts +9 -0
- package/src/replay/Replay.ts +92 -16
- package/src/replay/Trailers.ts +17 -0
- package/src/replay/Tree.ts +5 -0
- package/src/replay/index.ts +1 -1
- package/src/steering/FORMATS.md +3 -1
- package/src/steering/SteeringFormat.ts +13 -0
- package/src/steering/fixtures/review/sectioned.md +1 -1
- package/src/steering/fixtures/review/sectioned.read.json +0 -1
- package/src/steering/index.ts +1 -0
- package/src/steering/review.test.ts +181 -5
- package/src/steering/review.ts +160 -18
- package/src/step/LandStep.ts +12 -0
- package/src/step/RepoSnapshot.ts +15 -0
- package/src/step/index.ts +1 -0
- package/src/step/planStart.test.ts +4 -3
- package/src/step/planStart.ts +7 -0
- package/src/step/planStep.ts +22 -17
- package/src/step/startBranch.ts +54 -0
- package/src/twins/EmittedScriptRecognizer.ts +118 -1
- package/src/twins/EmittedScripts.test.ts +147 -1
- package/src/twins/InMemRepo.ts +140 -3
- package/src/twins/Layers.ts +1 -0
- package/src/twins/SyncIo.test.ts +19 -4
- package/src/twins/SyncIo.ts +13 -1
- package/src/wire/BeatDocument.test.ts +15 -0
- package/src/wire/BeatDocument.ts +8 -2
- package/src/wire/Demand.ts +26 -12
- package/src/wire/constants.ts +3 -0
- package/src/wire/golden/1/beat-capture.json +1 -0
- package/src/wire/golden/1/beat-message.json +1 -0
- package/src/wire/golden/1/beat-prompt.json +1 -0
- package/src/wire/golden/1/beat-script.json +1 -0
- package/src/wire/golden/1/beat-stalled.json +1 -0
- package/src/wire/types.ts +4 -0
- package/src/workflow/Workflow.ts +3 -0
- package/src/workflow/compile.ts +2 -0
- package/src/workflow/load.ts +2 -0
- package/src/workflow-kit/index.test.ts +1 -0
- package/src/workflow-kit/index.ts +1 -0
- package/src/workflow-kit/scenarios.ts +8 -0
- package/claude/hooks/ship.ts +0 -730
package/src/cli/History.test.ts
CHANGED
|
@@ -10,6 +10,9 @@ import {
|
|
|
10
10
|
import { runCli } from "./index.js"
|
|
11
11
|
import { historyEntries, resolveHistoryRef, type HistoryEntry } from "./History.js"
|
|
12
12
|
|
|
13
|
+
// A process runs on `gtd/<b>`; the recorded branch is the user's `<b>`.
|
|
14
|
+
const USER_BRANCH = "main"
|
|
15
|
+
|
|
13
16
|
const CONFIG = `import { agent, human } from "@pmelab/gtd/flows"
|
|
14
17
|
|
|
15
18
|
export default async () => {
|
|
@@ -32,8 +35,9 @@ const land = async (repo: InMemRepo, files: Record<string, string>): Promise<str
|
|
|
32
35
|
(JSON.parse(stdout) as { script: string }).script,
|
|
33
36
|
)
|
|
34
37
|
if (!outcome.ok) throw new Error(outcome.error)
|
|
38
|
+
const branch = repo.branchName()
|
|
35
39
|
await takePinBeat(repo, "remove")
|
|
36
|
-
return repo.resolveRef(
|
|
40
|
+
return repo.resolveRef(`refs/heads/${branch}`)!
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
const finishedProcess = async (repo: InMemRepo, file: string) => {
|
|
@@ -82,7 +86,7 @@ describe("historyEntries", () => {
|
|
|
82
86
|
expect(list.map((e) => e.entry)).toEqual([second.entry, first.entry])
|
|
83
87
|
expect(list[0]).toMatchObject({
|
|
84
88
|
ref: `refs/gtd/history/${second.entry}`,
|
|
85
|
-
branch:
|
|
89
|
+
branch: USER_BRANCH,
|
|
86
90
|
tip: second.tip,
|
|
87
91
|
subject: "gtd(human): idle → building",
|
|
88
92
|
})
|
|
@@ -169,16 +173,14 @@ describe("gtd history list", () => {
|
|
|
169
173
|
const text = await gtd(repo, "history", "list")
|
|
170
174
|
const line = text.stdout.trimEnd().split("\n")
|
|
171
175
|
expect(line).toHaveLength(1)
|
|
172
|
-
expect(
|
|
173
|
-
line[0]!.startsWith(`${hash.slice(0, 7)} ${repo.branchName()} ${tip.slice(0, 7)} `),
|
|
174
|
-
).toBe(true)
|
|
176
|
+
expect(line[0]!.startsWith(`${hash.slice(0, 7)} ${USER_BRANCH} ${tip.slice(0, 7)} `)).toBe(true)
|
|
175
177
|
expect(line[0]!.endsWith(" gtd(human): idle → building")).toBe(true)
|
|
176
178
|
const json = await gtd(repo, "history", "list", "--json")
|
|
177
179
|
expect(JSON.parse(json.stdout)).toEqual([
|
|
178
180
|
expect.objectContaining({
|
|
179
181
|
ref: `refs/gtd/history/${hash}`,
|
|
180
182
|
entry: hash,
|
|
181
|
-
branch:
|
|
183
|
+
branch: USER_BRANCH,
|
|
182
184
|
tip,
|
|
183
185
|
subject: "gtd(human): idle → building",
|
|
184
186
|
}),
|
|
@@ -195,7 +197,7 @@ describe("gtd history show", () => {
|
|
|
195
197
|
expect(out.exitCode).toBe(0)
|
|
196
198
|
const lines = out.stdout.trimEnd().split("\n")
|
|
197
199
|
expect(lines[0]).toBe(
|
|
198
|
-
`refs/gtd/history/${hash} (branch ${
|
|
200
|
+
`refs/gtd/history/${hash} (branch ${USER_BRANCH}, tip ${tip.slice(0, 7)})`,
|
|
199
201
|
)
|
|
200
202
|
expect(out.stdout).toContain("gtd(human): idle → building")
|
|
201
203
|
expect(lines[lines.length - 1]).toBe("ended (return)")
|
|
@@ -206,7 +208,7 @@ describe("gtd history show", () => {
|
|
|
206
208
|
const { entry: hash, tip } = await finishedProcess(repo, "a.txt")
|
|
207
209
|
const out = await gtd(repo, "history", "show", hash, "--json")
|
|
208
210
|
const doc = JSON.parse(out.stdout)
|
|
209
|
-
expect(doc).toMatchObject({ entry: hash, tip, ended: "return", branch:
|
|
211
|
+
expect(doc).toMatchObject({ entry: hash, tip, ended: "return", branch: USER_BRANCH })
|
|
210
212
|
expect(doc.steps.length).toBeGreaterThan(0)
|
|
211
213
|
expect(doc.version).toHaveProperty("running")
|
|
212
214
|
})
|
package/src/cli/Metrics.test.ts
CHANGED
|
@@ -233,8 +233,9 @@ const land = async (repo: InMemRepo, files: Record<string, string>): Promise<str
|
|
|
233
233
|
(JSON.parse(stdout) as { script: string }).script,
|
|
234
234
|
)
|
|
235
235
|
if (!outcome.ok) throw new Error(outcome.error)
|
|
236
|
+
const branch = repo.branchName()
|
|
236
237
|
await takePinBeat(repo, "remove")
|
|
237
|
-
return repo.resolveRef(
|
|
238
|
+
return repo.resolveRef(`refs/heads/${branch}`)!
|
|
238
239
|
}
|
|
239
240
|
|
|
240
241
|
describe("readProcessMetrics", () => {
|
package/src/cli/Program.test.ts
CHANGED
|
@@ -1694,7 +1694,9 @@ export default async () => {
|
|
|
1694
1694
|
const { exitCode, script } = await landAndApply(repo)
|
|
1695
1695
|
expect(exitCode).toBe(0)
|
|
1696
1696
|
expect(script).not.toContain("fmt-notes")
|
|
1697
|
-
expect(repo.lastCommitSubject("
|
|
1697
|
+
expect(repo.lastCommitSubject(`${repo.resolveRef("gtd/main")}~1`)).toBe(
|
|
1698
|
+
"gtd(agent): drafting → gtd:unpin",
|
|
1699
|
+
)
|
|
1698
1700
|
})
|
|
1699
1701
|
|
|
1700
1702
|
it("`gtd next --json`'s `validate` field carries the mode's format command", async () => {
|
|
@@ -1974,7 +1976,7 @@ export default async () => {
|
|
|
1974
1976
|
it("refuses once the process has closed and HEAD rests at the initial step again", async () => {
|
|
1975
1977
|
const repo = await atDeciding()
|
|
1976
1978
|
await landTurn(repo)
|
|
1977
|
-
expect(repo.lastCommitSubject()).toBe("gtd(check): gtd:unpin → idle")
|
|
1979
|
+
expect(repo.lastCommitSubject("gtd/main")).toBe("gtd(check): gtd:unpin → idle")
|
|
1978
1980
|
|
|
1979
1981
|
const { exitCode, stdout, stderr } = await run(repo, "base")
|
|
1980
1982
|
expect(exitCode).toBe(1)
|
package/src/cli/Program.ts
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
type GitOperations,
|
|
10
10
|
type HostOps,
|
|
11
11
|
HISTORY_REF,
|
|
12
|
+
RESUME_BRANCH_REF,
|
|
13
|
+
processHistoryRef,
|
|
12
14
|
readRetainedHistory,
|
|
13
15
|
restorability,
|
|
14
16
|
loopLogPath,
|
|
@@ -67,8 +69,14 @@ import {
|
|
|
67
69
|
summaryRun,
|
|
68
70
|
type RenderedRest,
|
|
69
71
|
} from "../edge/index.js"
|
|
70
|
-
import { formatCommitMessage, PIN_STATES } from "../replay/index.js"
|
|
71
|
-
import {
|
|
72
|
+
import { formatCommitMessage, parseCommitMessage, PIN_STATES } from "../replay/index.js"
|
|
73
|
+
import {
|
|
74
|
+
GTD_BRANCH_PREFIX,
|
|
75
|
+
userBranchOf,
|
|
76
|
+
planStart,
|
|
77
|
+
planStep as planStepPure,
|
|
78
|
+
type JudgeVerdict,
|
|
79
|
+
} from "../step/index.js"
|
|
72
80
|
import {
|
|
73
81
|
BUILT_IN_MODE_NAMES,
|
|
74
82
|
formatFromSpec,
|
|
@@ -85,6 +93,10 @@ import {
|
|
|
85
93
|
deleteRef,
|
|
86
94
|
hardResetTo,
|
|
87
95
|
mixedResetTo,
|
|
96
|
+
switchBranchTo,
|
|
97
|
+
switchTo,
|
|
98
|
+
deleteBranch,
|
|
99
|
+
pushRef,
|
|
88
100
|
unpinScript,
|
|
89
101
|
repinScript,
|
|
90
102
|
commitPath,
|
|
@@ -749,6 +761,29 @@ const runStartListCommand = (
|
|
|
749
761
|
)
|
|
750
762
|
})
|
|
751
763
|
|
|
764
|
+
/**
|
|
765
|
+
* Back on `<b>` when the process ran on `gtd/<b>` and `<b>` moved only by its
|
|
766
|
+
* ships (whose `Gtd-Tip` the tip descends from): the rewind stops at the last
|
|
767
|
+
* ship, so the uncommitted changes are what it left unshipped.
|
|
768
|
+
*/
|
|
769
|
+
const abandonTarget = (current: Option.Option<string>, tip: string, startParent: string) =>
|
|
770
|
+
Effect.gen(function* () {
|
|
771
|
+
const git = yield* GitService
|
|
772
|
+
const userBranch = Option.isSome(current) ? userBranchOf(current.value) : undefined
|
|
773
|
+
const userTip =
|
|
774
|
+
userBranch === undefined
|
|
775
|
+
? Option.none<string>()
|
|
776
|
+
: yield* git.readRefOption(`refs/heads/${userBranch}`)
|
|
777
|
+
if (userBranch === undefined || Option.isNone(userTip)) {
|
|
778
|
+
return { leaves: undefined, rewindTo: startParent }
|
|
779
|
+
}
|
|
780
|
+
if (userTip.value === startParent) return { leaves: userBranch, rewindTo: startParent }
|
|
781
|
+
const shipped = parseCommitMessage(yield* git.lastCommitMessage(userTip.value)).tip
|
|
782
|
+
return shipped !== undefined && (yield* git.isAncestor(shipped, tip))
|
|
783
|
+
? { leaves: userBranch, rewindTo: userTip.value }
|
|
784
|
+
: { leaves: undefined, rewindTo: startParent }
|
|
785
|
+
})
|
|
786
|
+
|
|
752
787
|
/**
|
|
753
788
|
* Ends the process underway without completing it. Nothing is discarded: a
|
|
754
789
|
* `git reset --mixed` to the process's start commit leaves every turn
|
|
@@ -766,6 +801,7 @@ const runAbandonCommand = (
|
|
|
766
801
|
json: JsonMode,
|
|
767
802
|
out: ArtifactOut,
|
|
768
803
|
): Effect.Effect<void, Error, CommandRequirements> =>
|
|
804
|
+
// fallow-ignore-next-line complexity
|
|
769
805
|
Effect.gen(function* () {
|
|
770
806
|
const git = yield* GitService
|
|
771
807
|
const config = yield* (yield* ConfigService).load
|
|
@@ -790,17 +826,58 @@ const runAbandonCommand = (
|
|
|
790
826
|
}
|
|
791
827
|
|
|
792
828
|
const tip = yield* git.resolveRef("HEAD")
|
|
829
|
+
const historyRef = processHistoryRef(run.startHash)
|
|
830
|
+
|
|
831
|
+
const current = yield* git.currentBranch()
|
|
832
|
+
const { leaves, rewindTo } = yield* abandonTarget(current, tip, run.startParentHash)
|
|
833
|
+
const remote = config.historyRemote
|
|
793
834
|
|
|
794
835
|
const steps: EmitStep[] = [
|
|
795
836
|
{ kind: "gitWrite", command: updateRef(HISTORY_REF, tip) },
|
|
796
|
-
{ kind: "gitWrite", command:
|
|
837
|
+
{ kind: "gitWrite", command: updateRef(historyRef, tip) },
|
|
838
|
+
{ kind: "gitWrite", command: mixedResetTo(rewindTo) },
|
|
797
839
|
{ kind: "gitWrite", command: unpinScript(PIN_PATH) },
|
|
798
|
-
|
|
840
|
+
...(leaves !== undefined
|
|
841
|
+
? [
|
|
842
|
+
{ kind: "gitWrite", command: switchTo(leaves) } as const,
|
|
843
|
+
{
|
|
844
|
+
kind: "gitWrite",
|
|
845
|
+
command: deleteBranch(Option.getOrThrow(current)),
|
|
846
|
+
} as const,
|
|
847
|
+
{ kind: "gitWrite", command: updateRef(RESUME_BRANCH_REF, tip) } as const,
|
|
848
|
+
]
|
|
849
|
+
: [{ kind: "gitWrite", command: deleteRef(RESUME_BRANCH_REF) } as const]),
|
|
850
|
+
{ kind: "outcome", command: abandonedOutcome(restState, rewindTo, initial) },
|
|
851
|
+
...(remote !== undefined
|
|
852
|
+
? [{ kind: "gitWrite", command: pushRef(remote, historyRef) } as const]
|
|
853
|
+
: []),
|
|
799
854
|
]
|
|
800
855
|
const required = emitScripts(steps).required
|
|
801
856
|
yield* writeScript(json, out, combinedScript(required, ""))
|
|
802
857
|
})
|
|
803
858
|
|
|
859
|
+
/**
|
|
860
|
+
* `gtd/<b>` when the abandon left it for `<b>` and HEAD is still on `<b>`. A
|
|
861
|
+
* `gtd/<b>` made since is reset only when the retained tip contains it — the
|
|
862
|
+
* `-C` switch would orphan anything else.
|
|
863
|
+
*/
|
|
864
|
+
const resumeBranch = (git: GitOperations, tip: string): Effect.Effect<string | undefined, Error> =>
|
|
865
|
+
Effect.gen(function* () {
|
|
866
|
+
if (Option.isNone(yield* git.readRefOption(RESUME_BRANCH_REF))) return undefined
|
|
867
|
+
const current = yield* git.currentBranch()
|
|
868
|
+
if (Option.isNone(current) || current.value.startsWith(GTD_BRANCH_PREFIX)) return undefined
|
|
869
|
+
const branch = `${GTD_BRANCH_PREFIX}${current.value}`
|
|
870
|
+
const existing = yield* git.readRefOption(`refs/heads/${branch}`)
|
|
871
|
+
if (Option.isSome(existing) && !(yield* git.isAncestor(existing.value, tip))) {
|
|
872
|
+
return yield* Effect.fail(
|
|
873
|
+
new Error(
|
|
874
|
+
`gtd restore: branch ${branch} holds commits the retained tip ${tip.slice(0, 7)} does not — merge or delete it first`,
|
|
875
|
+
),
|
|
876
|
+
)
|
|
877
|
+
}
|
|
878
|
+
return branch
|
|
879
|
+
})
|
|
880
|
+
|
|
804
881
|
/**
|
|
805
882
|
* `gtd restore`: hard-reset HEAD back to the tip `gtd abandon` retained.
|
|
806
883
|
*
|
|
@@ -834,7 +911,10 @@ const runRestoreCommand = (
|
|
|
834
911
|
const tip = retained.value
|
|
835
912
|
|
|
836
913
|
const headHash = yield* git.resolveRef("HEAD")
|
|
837
|
-
|
|
914
|
+
// Abandon after a ship leaves HEAD on `<b>` at the bare ship commit: its `Gtd-Tip` is on the retained tip.
|
|
915
|
+
const shippedTip = parseCommitMessage(yield* git.lastCommitMessage(headHash)).tip
|
|
916
|
+
const atShip = shippedTip !== undefined && (yield* git.isAncestor(shippedTip, tip))
|
|
917
|
+
const check = atShip ? { ok: true as const } : yield* restorability(git, headHash, tip)
|
|
838
918
|
if (!check.ok) {
|
|
839
919
|
return yield* Effect.fail(
|
|
840
920
|
new Error(
|
|
@@ -847,9 +927,14 @@ const runRestoreCommand = (
|
|
|
847
927
|
// A preview of the resulting state, resolved at tip directly since no reset has happened yet.
|
|
848
928
|
const after = yield* restAt(tip)
|
|
849
929
|
|
|
930
|
+
const resumeOn = yield* resumeBranch(git, tip)
|
|
850
931
|
const steps: EmitStep[] = [
|
|
851
|
-
{
|
|
932
|
+
{
|
|
933
|
+
kind: "gitWrite",
|
|
934
|
+
command: resumeOn === undefined ? hardResetTo(tip) : switchBranchTo(resumeOn, tip),
|
|
935
|
+
},
|
|
852
936
|
{ kind: "gitWrite", command: deleteRef(HISTORY_REF) },
|
|
937
|
+
{ kind: "gitWrite", command: deleteRef(RESUME_BRANCH_REF) },
|
|
853
938
|
{ kind: "outcome", command: restoredOutcome(tip, after.state) },
|
|
854
939
|
]
|
|
855
940
|
const required = emitScripts(steps).required
|
|
@@ -1204,6 +1289,17 @@ const computeNextMatch = (rest: Rest): Effect.Effect<NextMatch | null> =>
|
|
|
1204
1289
|
(target) => (target === undefined ? null : { target }),
|
|
1205
1290
|
)
|
|
1206
1291
|
|
|
1292
|
+
/** A validator retry's content is the correction; a session that cannot resume needs the original prompt too, and one that can hands a driver that loses it `fresh`. */
|
|
1293
|
+
const retryContent = (
|
|
1294
|
+
kind: BeatKind,
|
|
1295
|
+
rendered: RenderedRest,
|
|
1296
|
+
resumes: boolean,
|
|
1297
|
+
): { readonly rendered: RenderedRest; readonly fresh?: string } => {
|
|
1298
|
+
if (kind !== "prompt" || rendered.original === undefined) return { rendered }
|
|
1299
|
+
const joined = `${rendered.original}\n\n${rendered.content}`
|
|
1300
|
+
return resumes ? { rendered, fresh: joined } : { rendered: { ...rendered, content: joined } }
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1207
1303
|
/** Everything one beat needs beyond the resolved rest itself, gathered once so plain/`--json` can never describe different rests for the same beat — built as `wire`'s `Demand` (what to do) and `BeatStatus` (what no driver branches on), then flattened by `beatDocument` into the one wire-shaped object plain/`--json` both render from. */
|
|
1208
1304
|
const gatherBeatDocument = (
|
|
1209
1305
|
rest: Rest,
|
|
@@ -1222,9 +1318,11 @@ const gatherBeatDocument = (
|
|
|
1222
1318
|
? resolvedValidate.script
|
|
1223
1319
|
: undefined
|
|
1224
1320
|
const log = yield* loopLogPath
|
|
1321
|
+
const retry = retryContent(kind, rendered, session?.resume === true)
|
|
1225
1322
|
const demand = demandOf({
|
|
1226
|
-
rendered,
|
|
1323
|
+
rendered: retry.rendered,
|
|
1227
1324
|
kind,
|
|
1325
|
+
...(retry.fresh !== undefined ? { fresh: retry.fresh } : {}),
|
|
1228
1326
|
...(session !== undefined
|
|
1229
1327
|
? { session: { id: session.sessionId, resume: session.resume } }
|
|
1230
1328
|
: {}),
|
package/src/edge/Edge.test.ts
CHANGED
|
@@ -100,7 +100,8 @@ const applied = (repo: InMemRepo, script: string): string => {
|
|
|
100
100
|
/**
|
|
101
101
|
* Lands the pending turn the way a driver does — `gtd land --json`, then its
|
|
102
102
|
* script, then the pin or unpin beat it leads to — and returns the turn's
|
|
103
|
-
* commit, or the closing commit when the turn led to the unpin
|
|
103
|
+
* commit, or the closing commit when the turn led to the unpin (which leaves
|
|
104
|
+
* HEAD back on the user's branch).
|
|
104
105
|
*/
|
|
105
106
|
const land = async (repo: InMemRepo, files: Files = {}): Promise<string> => {
|
|
106
107
|
write(repo, files)
|
|
@@ -108,7 +109,8 @@ const land = async (repo: InMemRepo, files: Files = {}): Promise<string> => {
|
|
|
108
109
|
if (exitCode !== 0) throw new Error(`gtd land exited ${exitCode}: ${stderr}${stdout}`)
|
|
109
110
|
const turn = applied(repo, (JSON.parse(stdout) as { readonly script: string }).script)
|
|
110
111
|
await takePinBeat(repo, "add")
|
|
111
|
-
|
|
112
|
+
const branch = repo.branchName()
|
|
113
|
+
return (await takePinBeat(repo, "remove")) ? headOf(repo, `refs/heads/${branch}`) : turn
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
const enter = async (repo: InMemRepo, workflow: string, ...vars: string[]): Promise<string> => {
|
|
@@ -206,7 +208,8 @@ describe("currentRun", () => {
|
|
|
206
208
|
await land(repo, { "a.txt": "a\n" })
|
|
207
209
|
await land(repo, { "b.txt": "b\n" })
|
|
208
210
|
const closing = await land(repo, { "c.txt": "c\n" })
|
|
209
|
-
expect(repo.lastCommitSubject()).toBe("gtd(check): gtd:unpin → idle")
|
|
211
|
+
expect(repo.lastCommitSubject(closing)).toBe("gtd(check): gtd:unpin → idle")
|
|
212
|
+
repo.checkoutBranch("gtd/main")
|
|
210
213
|
const next = await land(repo, { "d.txt": "d\n" })
|
|
211
214
|
const run = await provide(currentRun, repo)
|
|
212
215
|
expect(run.startParentHash).toBe(closing)
|
|
@@ -413,7 +416,12 @@ export const again = workflow(async () => {
|
|
|
413
416
|
await land(repo, { "a.txt": "a\n" })
|
|
414
417
|
await land(repo, { "b.txt": "b\n" })
|
|
415
418
|
const run = await provide(currentRun, repo)
|
|
416
|
-
expect(run.headTurn).toEqual({
|
|
419
|
+
expect(run.headTurn).toEqual({
|
|
420
|
+
state: "checking",
|
|
421
|
+
actor: "agent",
|
|
422
|
+
empty: false,
|
|
423
|
+
step: true,
|
|
424
|
+
})
|
|
417
425
|
})
|
|
418
426
|
|
|
419
427
|
it("is undefined at a pin commit, which is no turn", async () => {
|
|
@@ -429,7 +437,12 @@ export const again = workflow(async () => {
|
|
|
429
437
|
await land(repo)
|
|
430
438
|
expect(repo.lastCommitMessage()).toBe("gtd(agent): building\n\nGtd-Format: 4")
|
|
431
439
|
const run = await provide(currentRun, repo)
|
|
432
|
-
expect(run.headTurn).toEqual({
|
|
440
|
+
expect(run.headTurn).toEqual({
|
|
441
|
+
state: "building",
|
|
442
|
+
actor: "agent",
|
|
443
|
+
empty: true,
|
|
444
|
+
step: false,
|
|
445
|
+
})
|
|
433
446
|
})
|
|
434
447
|
|
|
435
448
|
it("is undefined at a non-gtd HEAD", async () => {
|
|
@@ -560,7 +573,7 @@ describe("summaryRun", () => {
|
|
|
560
573
|
|
|
561
574
|
const ordinary = await provide(currentRun, repo)
|
|
562
575
|
expect(ordinary.trace).toEqual([])
|
|
563
|
-
expect(ordinary.startParentHash).toBe(
|
|
576
|
+
expect(ordinary.startParentHash).toBe(boundary)
|
|
564
577
|
|
|
565
578
|
const summary = await provide(summaryRun, repo)
|
|
566
579
|
expect(summary.closingHash).toBe(closing)
|
|
@@ -645,14 +658,17 @@ describe("process marks", () => {
|
|
|
645
658
|
})
|
|
646
659
|
})
|
|
647
660
|
|
|
648
|
-
const withTipMessage = (
|
|
661
|
+
const withTipMessage = (
|
|
662
|
+
repo: InMemRepo,
|
|
663
|
+
edit: (message: string) => string,
|
|
664
|
+
edited = repo.resolveRef("HEAD"),
|
|
665
|
+
): GitOperations => {
|
|
649
666
|
const real = fakeGitOperations(repo)
|
|
650
|
-
const head = repo.resolveRef("HEAD")
|
|
651
667
|
return {
|
|
652
668
|
...real,
|
|
653
669
|
commitHistory: (base, tip) =>
|
|
654
670
|
Effect.map(real.commitHistory(base, tip), (commits) =>
|
|
655
|
-
commits.map((c) => (c.hash ===
|
|
671
|
+
commits.map((c) => (c.hash === edited ? { ...c, message: edit(c.message) } : c)),
|
|
656
672
|
),
|
|
657
673
|
}
|
|
658
674
|
}
|
|
@@ -683,7 +699,8 @@ export default workflow(async () => {
|
|
|
683
699
|
if (exitCode !== 0) throw new Error(`gtd land exited ${exitCode}: ${stderr}${stdout}`)
|
|
684
700
|
const turn = applied(repo, (JSON.parse(stdout) as { readonly script: string }).script)
|
|
685
701
|
await takePinBeat(repo, "add")
|
|
686
|
-
|
|
702
|
+
const branch = repo.branchName()
|
|
703
|
+
return (await takePinBeat(repo, "remove")) ? headOf(repo, `refs/heads/${branch}`) : turn
|
|
687
704
|
}
|
|
688
705
|
type Rendered = {
|
|
689
706
|
entryCommit: string
|
|
@@ -1366,7 +1383,7 @@ describe("finishing landings keep history", () => {
|
|
|
1366
1383
|
expect(landing.spec.pin).toBe("remove")
|
|
1367
1384
|
expect(landing.keepHistory).toBe(processHistoryRef(building))
|
|
1368
1385
|
expect(landing.spec.version).toBe(GTD_VERSION)
|
|
1369
|
-
expect(landing.spec.branch).toBe(
|
|
1386
|
+
expect(landing.spec.branch).toBe("main")
|
|
1370
1387
|
})
|
|
1371
1388
|
|
|
1372
1389
|
it("restart() is a finish too", async () => {
|
|
@@ -1647,8 +1664,10 @@ export default async () => {
|
|
|
1647
1664
|
|
|
1648
1665
|
it("warns, never refuses, when the recorded version differs", async () => {
|
|
1649
1666
|
const { repo, tip } = await finished()
|
|
1650
|
-
const git = withTipMessage(
|
|
1651
|
-
|
|
1667
|
+
const git = withTipMessage(
|
|
1668
|
+
repo,
|
|
1669
|
+
(m) => m.replace(`Gtd-Version: ${GTD_VERSION}`, "Gtd-Version: 0.0.1"),
|
|
1670
|
+
tip,
|
|
1652
1671
|
)
|
|
1653
1672
|
const trail = await Effect.runPromise(
|
|
1654
1673
|
historyTrail(tip).pipe(
|
|
@@ -1663,7 +1682,7 @@ export default async () => {
|
|
|
1663
1682
|
|
|
1664
1683
|
it("a missing recorded version counts as different", async () => {
|
|
1665
1684
|
const { repo, tip } = await finished()
|
|
1666
|
-
const git = withTipMessage(repo, (m) => m.replace(/Gtd-Version: .*\n?/, ""))
|
|
1685
|
+
const git = withTipMessage(repo, (m) => m.replace(/Gtd-Version: .*\n?/, ""), tip)
|
|
1667
1686
|
const trail = await Effect.runPromise(
|
|
1668
1687
|
historyTrail(tip).pipe(
|
|
1669
1688
|
Effect.provideService(GitService, git),
|
|
@@ -1677,8 +1696,10 @@ export default async () => {
|
|
|
1677
1696
|
it("reads a recorded version carrying control bytes as unknown", async () => {
|
|
1678
1697
|
const { repo, tip } = await finished()
|
|
1679
1698
|
const raw = "1.0\u001b]0;x\u0007"
|
|
1680
|
-
const git = withTipMessage(
|
|
1681
|
-
|
|
1699
|
+
const git = withTipMessage(
|
|
1700
|
+
repo,
|
|
1701
|
+
(m) => m.replace(`Gtd-Version: ${GTD_VERSION}`, `Gtd-Version: ${raw}`),
|
|
1702
|
+
tip,
|
|
1682
1703
|
)
|
|
1683
1704
|
const trail = await Effect.runPromise(
|
|
1684
1705
|
historyTrail(tip).pipe(
|