@pmelab/gtd 64.2.0 → 65.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 +36 -26
- package/claude/hooks/assist.ts +9 -35
- package/claude/hooks/doors.ts +4 -4
- 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 +8 -8
- package/claude/hooks/register.tsx +16 -78
- package/claude/hooks/tools.ts +7 -20
- package/claude/hooks/wording.ts +3 -3
- package/dist/chunks/{flows-dnIE_qbe.mjs → flows-DGs0W3HI.mjs} +19 -2
- package/dist/chunks/{testkit-BwCH8ky6.mjs → testkit-CgrLmpdJ.mjs} +741 -80
- package/dist/chunks/{wire-CaPJ9Tme.mjs → wire-BNS8ciNu.mjs} +5 -3
- package/dist/flows.mjs +1 -1
- package/dist/gtd.bundle.mjs +565 -114
- package/dist/gtd.core.mjs +132 -129
- package/dist/testkit/gherkin.mjs +1 -1
- package/dist/testkit.mjs +2 -2
- package/dist/web/main.js +2 -2
- package/dist/wire.mjs +1 -1
- package/dist/workflow-kit.mjs +1 -1
- 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 +87 -5
- package/src/edge/Edge.test.ts +37 -16
- package/src/edge/Edge.ts +296 -31
- package/src/emit/GitScript.ts +116 -15
- package/src/emit/index.ts +7 -0
- package/src/flows/config.ts +7 -1
- package/src/flows/index.test.ts +1 -0
- package/src/flows/index.ts +2 -0
- package/src/flows/runtime.ts +32 -1
- package/src/platform/RetainedHistory.ts +3 -0
- package/src/platform/index.ts +1 -0
- package/src/replay/Replay.test.ts +9 -0
- package/src/replay/Replay.ts +59 -15
- package/src/replay/Trailers.ts +14 -0
- package/src/replay/index.ts +1 -1
- package/src/step/LandStep.ts +12 -0
- package/src/step/RepoSnapshot.ts +11 -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 +17 -12
- package/src/step/startBranch.ts +54 -0
- package/src/twins/EmittedScriptRecognizer.ts +117 -0
- package/src/twins/EmittedScripts.test.ts +147 -1
- package/src/twins/InMemRepo.ts +116 -0
- package/src/wire/Demand.ts +22 -10
- package/src/wire/constants.ts +3 -0
- package/src/wire/types.ts +4 -0
- package/src/workflow/Workflow.ts +1 -0
- package/src/workflow/compile.ts +2 -0
- package/src/workflow/load.ts +2 -0
- package/claude/hooks/ship.ts +0 -730
package/dist/testkit/gherkin.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as runsItsScript, r as testRepo } from "../chunks/testkit-
|
|
1
|
+
import { n as runsItsScript, r as testRepo } from "../chunks/testkit-CgrLmpdJ.mjs";
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { After, AfterAll, Given, QuickPickleWorld, Then, When, setWorldConstructor } from "quickpickle";
|
package/dist/testkit.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { H as installContext } from "./chunks/flows-
|
|
2
|
-
import { a as verdictWhen, i as verdict, n as runsItsScript, o as whenPrompted, r as testRepo, t as inTurn } from "./chunks/testkit-
|
|
1
|
+
import { H as installContext } from "./chunks/flows-DGs0W3HI.mjs";
|
|
2
|
+
import { a as verdictWhen, i as verdict, n as runsItsScript, o as whenPrompted, r as testRepo, t as inTurn } from "./chunks/testkit-CgrLmpdJ.mjs";
|
|
3
3
|
export { inTurn, installContext, runsItsScript, testRepo, verdict, verdictWhen, whenPrompted };
|
package/dist/web/main.js
CHANGED
|
@@ -35949,8 +35949,8 @@ const openable = (step) => step?.status === "ok" && step.file !== void 0;
|
|
|
35949
35949
|
* `"qa"` means `Plan`; `"findings"` means `Findings`; anything else (absent, or an unregistered mode name)
|
|
35950
35950
|
* falls through to `FreeForm`, which reads the file as plain markdown and
|
|
35951
35951
|
* offers only a note per block, plus a textfield when it's empty. Neither
|
|
35952
|
-
* screen offers a way back to a list — `done`
|
|
35953
|
-
*
|
|
35952
|
+
* screen offers a way back to a list — `done` hands the turn back (the
|
|
35953
|
+
* server keeps running until the outer loop moves on).
|
|
35954
35954
|
*
|
|
35955
35955
|
* Never returns `null`: a blank white screen was package 03's own regression
|
|
35956
35956
|
* (a phone over a tailnet with nothing on-screen to explain why) — every
|
package/dist/wire.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { O as ACCESS_REFUSAL, _ as BEAT_KINDS, f as DEMAND_BRIEFING, x as EXIT_CODES, S as EXIT_OK, C as EXIT_RUNTIME_ERROR, w as EXIT_SIGINT, T as EXIT_SIGTERM, E as EXIT_STRAYS_FOUND, D as EXIT_USAGE_ERROR, k as PIN_PATH, A as
|
|
1
|
+
export { O as ACCESS_REFUSAL, _ as BEAT_KINDS, f as DEMAND_BRIEFING, x as EXIT_CODES, S as EXIT_OK, C as EXIT_RUNTIME_ERROR, w as EXIT_SIGINT, T as EXIT_SIGTERM, E as EXIT_STRAYS_FOUND, D as EXIT_USAGE_ERROR, k as PIN_PATH, A as RUN_FAILED_PATH, j as UNATTRIBUTED_MODEL, M as WIRE_SCHEMA, m as beatDocument, v as beatKindOf, y as demandOf, t as globMatches, i as judgeJson, s as landFields, c as landProseText, l as noopText, h as renderBeatJson, p as renderBeatPlain, N as renderFormat, u as renderLandJson, d as renderLandPlain, a as renderScriptJson, n as resolveSession, o as scriptFields, r as selectPath, b as stallDiagnosis, g as statusOf } from "./chunks/wire-BNS8ciNu.mjs";
|
package/dist/workflow-kit.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as head, F as changesSince, G as mark, P as changes, X as read, Z as refuse, d as hasThreadFor, et as scope, g as requireReplies, v as requireThreadsClosed } from "./chunks/flows-
|
|
1
|
+
import { B as head, F as changesSince, G as mark, P as changes, X as read, Z as refuse, d as hasThreadFor, et as scope, g as requireReplies, v as requireThreadsClosed } from "./chunks/flows-DGs0W3HI.mjs";
|
|
2
2
|
import { C as reviewRisks, N as parseThreads, S as reviewNotes, T as unansweredNotes, n as stripCodeThreads, x as reviewDecisions } from "./chunks/steering-DEh_MS40.mjs";
|
|
3
3
|
// src/workflow-kit/health.ts
|
|
4
4
|
/** Hand a red suite that repeated attempts could not fix to a person, until `limit` rounds are spent. */
|
package/package.json
CHANGED
package/src/cli/Cli.ts
CHANGED
|
@@ -445,7 +445,7 @@ gtd writes nothing itself. Plain output names the commit it would make; --json=s
|
|
|
445
445
|
arity: { name: "name", variadic: "args", bare: true },
|
|
446
446
|
group: "Start and stop",
|
|
447
447
|
summary: "Start a workflow or door; bare, list them",
|
|
448
|
-
details: `Start a new process: print the script that makes its first commit, which also commits whatever the working tree holds.
|
|
448
|
+
details: `Start a new process: print the script that makes its first commit, which also commits whatever the working tree holds. The process runs on the branch \`gtd/<b>\` (created, or reused when nothing on it would be lost), never on \`<b>\`; a branch already named \`gtd/…\` and a detached HEAD start in place.
|
|
449
449
|
|
|
450
450
|
<name> is a door — a named shortcut whose positional args set the workflow's settings — or else a workflow the config exports, which takes no args. A door shadows a workflow of the same name. --var pins a setting either way.
|
|
451
451
|
|
|
@@ -459,7 +459,7 @@ With no <name>, list every workflow and door it can start. An unknown <name>, th
|
|
|
459
459
|
arity: "none",
|
|
460
460
|
group: "Start and stop",
|
|
461
461
|
summary: "Stop the process, keeping its changes",
|
|
462
|
-
details: `Stop the process underway without finishing it. The printed script moves HEAD back to the commit the process started from and keeps everything it produced as uncommitted changes
|
|
462
|
+
details: `Stop the process underway without finishing it. The printed script moves HEAD back to the commit the process started from, or to its last ship, and keeps everything it produced as uncommitted changes, switches back to \`<b>\` and deletes \`gtd/<b>\`; it keeps the abandoned tip at \`refs/gtd/history/<entry>\` (pushed to \`historyRemote\` when set). \`gtd restore\` undoes it. With no process underway the script does nothing.
|
|
463
463
|
|
|
464
464
|
s=$(gtd abandon --json=script) && sh -c "$s"`,
|
|
465
465
|
},
|
|
@@ -469,7 +469,7 @@ With no <name>, list every workflow and door it can start. An unknown <name>, th
|
|
|
469
469
|
arity: "none",
|
|
470
470
|
group: "Start and stop",
|
|
471
471
|
summary: "Bring back the last abandoned process",
|
|
472
|
-
details: `Bring back the process the last \`gtd abandon\` stopped: the printed script
|
|
472
|
+
details: `Bring back the process the last \`gtd abandon\` stopped: the printed script puts that process's last commit back, on a recreated \`gtd/<b>\` when the process ran on one. Refuses on a dirty working tree, when nothing was abandoned, or when commits made since would be lost.`,
|
|
473
473
|
},
|
|
474
474
|
{
|
|
475
475
|
token: "repin",
|
|
@@ -487,7 +487,7 @@ With no <name>, list every workflow and door it can start. An unknown <name>, th
|
|
|
487
487
|
arity: "none",
|
|
488
488
|
group: "Inspect",
|
|
489
489
|
summary: "Print the prompt for the closing message",
|
|
490
|
-
details: `Print the prompt for an agent to write the closing message of the process HEAD closes or sits inside: its first commit, each commit a person made, the diff range to read, and its cost. Writes nothing — what to do with the answer (a squash message, a PR body) is the driver's call.
|
|
490
|
+
details: `Print the prompt for an agent to write the closing message of the process HEAD closes or sits inside (back on a user branch, the one its gtd/ branch closes): its first commit, each commit a person made, the diff range to read, and its cost. Writes nothing — what to do with the answer (a squash message, a PR body) is the driver's call.
|
|
491
491
|
|
|
492
492
|
Refuses when the workflow declares no summary template, or when the process has no commits.`,
|
|
493
493
|
},
|
|
@@ -557,7 +557,7 @@ Prints a sorted JSON array ([] when clean) and a summary on stderr; --verbose ad
|
|
|
557
557
|
arity: "none",
|
|
558
558
|
group: "Inspect",
|
|
559
559
|
summary: "Serve the web and phone client",
|
|
560
|
-
details: `Serve gtd's web and phone client for this worktree. By default it publishes through \`tailscale serve\`, reachable from anywhere on the tailnet, on the first of ports 8443, 10000 and 443 that works; when none does it serves HTTPS locally instead of refusing.`,
|
|
560
|
+
details: `Serve gtd's web and phone client for this worktree. By default it publishes through \`tailscale serve\`, reachable from anywhere on the tailnet, on the first of ports 8443, 10000 and 443 that works; when none does it serves HTTPS locally instead of refusing. Done saves the steering file and keeps serving; the server exits when the worktree moves on to another step, or on a signal.`,
|
|
561
561
|
},
|
|
562
562
|
{
|
|
563
563
|
token: "guide",
|
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
|
*
|
|
@@ -847,9 +924,14 @@ const runRestoreCommand = (
|
|
|
847
924
|
// A preview of the resulting state, resolved at tip directly since no reset has happened yet.
|
|
848
925
|
const after = yield* restAt(tip)
|
|
849
926
|
|
|
927
|
+
const resumeOn = yield* resumeBranch(git, tip)
|
|
850
928
|
const steps: EmitStep[] = [
|
|
851
|
-
{
|
|
929
|
+
{
|
|
930
|
+
kind: "gitWrite",
|
|
931
|
+
command: resumeOn === undefined ? hardResetTo(tip) : switchBranchTo(resumeOn, tip),
|
|
932
|
+
},
|
|
852
933
|
{ kind: "gitWrite", command: deleteRef(HISTORY_REF) },
|
|
934
|
+
{ kind: "gitWrite", command: deleteRef(RESUME_BRANCH_REF) },
|
|
853
935
|
{ kind: "outcome", command: restoredOutcome(tip, after.state) },
|
|
854
936
|
]
|
|
855
937
|
const required = emitScripts(steps).required
|
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(
|