@pmelab/gtd 23.2.0 → 24.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +44 -11
- package/claude/hooks/access.ts +1 -1
- package/dist/chunks/Schema-Dh-mkwkq.mjs +25353 -0
- package/dist/chunks/reserved-ejn-LVYO.mjs +1388 -0
- package/dist/chunks/rolldown-runtime-8H4AJuhK.mjs +14 -0
- package/dist/chunks/steering-D-PvRC5W.mjs +16296 -0
- package/dist/chunks/wire-DA7q2ikx.mjs +473 -0
- package/dist/flows.mjs +525 -0
- package/dist/gtd.bundle.mjs +43830 -56044
- package/dist/steering.mjs +2 -0
- package/dist/testkit.mjs +13006 -0
- package/dist/wire.mjs +2 -0
- package/dist/workflow-kit.mjs +352 -0
- package/dist/workflows/feature.mjs +315 -0
- package/dist/workflows/fix.mjs +40 -0
- package/dist/workflows/review.mjs +25 -0
- package/dist/workflows.mjs +1953 -1724
- package/package.json +70 -8
- package/skills/authoring/SKILL.md +5 -4
- package/src/cli/Cli.test.ts +1329 -0
- package/src/cli/Cli.ts +1348 -0
- package/src/cli/Program.test.ts +2262 -0
- package/src/cli/Program.ts +1331 -0
- package/src/cli/ToolCommands.ts +43 -0
- package/src/cli/index.ts +14 -0
- package/src/edge/Edge.test.ts +1016 -0
- package/src/edge/Edge.ts +1225 -0
- package/src/edge/index.ts +16 -0
- package/src/emit/Emit.test.ts +425 -0
- package/src/emit/Emit.ts +139 -0
- package/src/emit/GitScript.test.ts +260 -0
- package/src/emit/GitScript.ts +131 -0
- package/src/emit/ModeContradiction.test.ts +120 -0
- package/src/emit/ModeContradiction.ts +66 -0
- package/src/emit/OutcomeScript.test.ts +238 -0
- package/src/emit/OutcomeScript.ts +73 -0
- package/src/emit/SteeringMode.test.ts +274 -0
- package/src/emit/SteeringMode.ts +256 -0
- package/src/emit/index.ts +40 -0
- package/src/flows/factory.ts +5 -1
- package/src/flows/runtime.ts +3 -0
- package/src/install/ConfigScaffold.ts +19 -0
- package/src/install/Install.test.ts +426 -0
- package/src/install/Install.ts +634 -0
- package/src/install/Version.ts +27 -0
- package/src/install/index.ts +3 -0
- package/src/platform/CommandRunner.ts +74 -0
- package/src/platform/Commentary.test.ts +126 -0
- package/src/platform/Commentary.ts +92 -0
- package/src/platform/Git.test.ts +50 -0
- package/src/platform/Git.ts +552 -0
- package/src/platform/Host.ts +42 -0
- package/src/platform/RetainedHistory.test.ts +65 -0
- package/src/platform/RetainedHistory.ts +34 -0
- package/src/platform/Workspace.test.ts +669 -0
- package/src/platform/Workspace.ts +631 -0
- package/src/platform/WorktreeState.test.ts +162 -0
- package/src/platform/WorktreeState.ts +50 -0
- package/src/platform/index.ts +22 -0
- package/src/replay/Access.test.ts +53 -0
- package/src/replay/Access.ts +46 -0
- package/src/replay/Format.test.ts +20 -0
- package/src/replay/Format.ts +14 -0
- package/src/replay/Replay.test.ts +887 -0
- package/src/replay/Replay.ts +669 -0
- package/src/replay/Trailers.test.ts +117 -0
- package/src/replay/Trailers.ts +213 -0
- package/src/replay/Tree.test.ts +25 -0
- package/src/replay/Tree.ts +45 -0
- package/src/replay/index.ts +11 -0
- package/src/steering/Blocks.fixture.ts +6 -0
- package/src/steering/Blocks.test.ts +167 -0
- package/src/steering/Blocks.ts +321 -0
- package/src/steering/CodeThreads.test.ts +127 -0
- package/src/steering/CodeThreads.ts +197 -0
- package/src/steering/Edits.test.ts +19 -0
- package/src/steering/Edits.ts +20 -0
- package/src/steering/Eol.test.ts +16 -0
- package/src/steering/Eol.ts +2 -0
- package/src/steering/FORMATS.md +58 -0
- package/src/steering/Footnotes.test.ts +977 -0
- package/src/steering/Footnotes.ts +661 -0
- package/src/steering/Inline.fixture.ts +7 -0
- package/src/steering/Inline.test.ts +227 -0
- package/src/steering/Inline.ts +195 -0
- package/src/steering/MarkdownTree.test.ts +247 -0
- package/src/steering/MarkdownTree.ts +260 -0
- package/src/steering/Outline.ts +19 -0
- package/src/steering/SteeringFormat.test.ts +91 -0
- package/src/steering/SteeringFormat.ts +267 -0
- package/src/steering/SteeringFormats.test.ts +163 -0
- package/src/steering/Threads.ts +163 -0
- package/src/steering/contract.test.ts +264 -0
- package/src/steering/fixtures/code-threads/cache.stripped.ts.txt +6 -0
- package/src/steering/fixtures/code-threads/cache.ts.md +15 -0
- package/src/steering/fixtures/free-form/notes.md +12 -0
- package/src/steering/fixtures/free-form/notes.read.json +42 -0
- package/src/steering/fixtures/qa/answered.md +23 -0
- package/src/steering/fixtures/qa/answered.read.json +119 -0
- package/src/steering/fixtures/qa/requirements.md +160 -0
- package/src/steering/fixtures/qa/requirements.read.json +475 -0
- package/src/steering/fixtures/review/review.md +76 -0
- package/src/steering/fixtures/review/review.read.json +237 -0
- package/src/steering/fixtures/review/ticked.md +23 -0
- package/src/steering/fixtures/review/ticked.read.json +99 -0
- package/src/steering/fixtures/review-notes/baseline.md +14 -0
- package/src/steering/fixtures/review-notes/noted.md +19 -0
- package/src/steering/freeform.test.ts +311 -0
- package/src/steering/freeform.ts +148 -0
- package/src/steering/index.test.ts +62 -0
- package/src/steering/index.ts +140 -0
- package/src/steering/qa.test.ts +2160 -0
- package/src/steering/qa.ts +1104 -0
- package/src/steering/review.test.ts +1829 -0
- package/src/steering/review.ts +1180 -0
- package/src/step/LandStep.ts +31 -0
- package/src/step/RepoSnapshot.ts +29 -0
- package/src/step/accessRefusal.test.ts +58 -0
- package/src/step/accessRefusal.ts +26 -0
- package/src/step/index.ts +5 -0
- package/src/step/planStart.test.ts +202 -0
- package/src/step/planStart.ts +167 -0
- package/src/step/planStep.test.ts +104 -0
- package/src/step/planStep.ts +89 -0
- package/src/step/snapshot.fixture.ts +28 -0
- package/src/testkit/EmittedScriptRecognizer.test.ts +639 -0
- package/src/testkit/EmittedScriptRecognizer.ts +642 -0
- package/src/testkit/FakeGitOperations.ts +95 -0
- package/src/testkit/GitTiers.test.ts +11 -0
- package/src/testkit/GitTiers.ts +1052 -0
- package/src/testkit/InMemRepo.ts +467 -0
- package/src/testkit/Layers.test.ts +58 -0
- package/src/testkit/Layers.ts +241 -0
- package/src/testkit/StrictGitOperations.ts +31 -0
- package/src/testkit/TestRepo.test.ts +119 -0
- package/src/testkit/TestRepo.ts +274 -0
- package/src/testkit/cliIo.ts +36 -0
- package/src/testkit/index.ts +9 -0
- package/src/testkit/steps.ts +177 -0
- package/src/testkit/testkit.feature +65 -0
- package/src/wire/BeatDocument.test.ts +419 -0
- package/src/wire/BeatDocument.ts +114 -0
- package/src/wire/BeatPlain.test.ts +113 -0
- package/src/wire/BeatPlain.ts +127 -0
- package/src/wire/BeatStatus.test.ts +59 -0
- package/src/wire/BeatStatus.ts +81 -0
- package/src/wire/Demand.test.ts +142 -0
- package/src/wire/Demand.ts +108 -0
- package/src/wire/ExitCodes.test.ts +21 -0
- package/src/wire/ExitCodes.ts +16 -0
- package/src/wire/Glob.ts +29 -0
- package/src/wire/Judge.ts +22 -0
- package/src/wire/Land.test.ts +120 -0
- package/src/wire/Land.ts +75 -0
- package/src/wire/Select.test.ts +148 -0
- package/src/wire/Select.ts +78 -0
- package/src/wire/Sessions.test.ts +59 -0
- package/src/wire/Sessions.ts +39 -0
- package/src/wire/WireSchema.test.ts +162 -0
- package/src/wire/constants.ts +22 -0
- package/src/wire/golden/1/beat-capture.json +34 -0
- package/src/wire/golden/1/beat-message.json +34 -0
- package/src/wire/golden/1/beat-prompt.json +44 -0
- package/src/wire/golden/1/beat-script.json +34 -0
- package/src/wire/golden/1/beat-stalled.json +34 -0
- package/src/wire/golden/1/judge.json +14 -0
- package/src/wire/golden/1/land-noop.json +10 -0
- package/src/wire/golden/1/land.json +10 -0
- package/src/wire/index.ts +12 -0
- package/src/wire/types.ts +46 -0
- package/src/workflow/Diagnostic.test.ts +93 -0
- package/src/workflow/Diagnostic.ts +54 -0
- package/src/workflow/SteeringFormats.test.ts +27 -0
- package/src/workflow/SteeringFormats.ts +6 -0
- package/src/workflow/Workflow.ts +77 -0
- package/src/workflow/compile.test.ts +53 -0
- package/src/workflow/compile.ts +56 -0
- package/src/workflow/discovery.ts +122 -0
- package/src/workflow/fingerprint.test.ts +52 -0
- package/src/workflow/fingerprint.ts +53 -0
- package/src/workflow/index.ts +33 -0
- package/src/workflow/load.test.ts +1017 -0
- package/src/workflow/load.ts +836 -0
- package/src/workflow/skills.test.ts +109 -0
- package/src/workflow/skills.ts +90 -0
- package/src/workflow/vars.ts +33 -0
- package/src/workflow-kit/health.test.ts +30 -0
- package/src/workflow-kit/health.ts +86 -0
- package/src/workflow-kit/index.ts +7 -0
- package/src/workflow-kit/quality.test.ts +17 -0
- package/src/workflow-kit/quality.ts +45 -0
- package/src/workflow-kit/review.ts +324 -0
- package/src/{workflows → workflow-kit}/scenarios.ts +21 -28
- package/src/workflows/feature/feature.test.ts +32 -0
- package/src/workflows/{bundled.ts → feature/feature.ts} +11 -63
- package/src/workflows/feature/index.ts +10 -0
- package/src/workflows/{packages.test.ts → feature/packages.test.ts} +2 -2
- package/src/workflows/{packages.ts → feature/packages.ts} +3 -3
- package/src/workflows/{planning.ts → feature/planning.ts} +2 -3
- package/src/workflows/{review.test.ts → feature/review.test.ts} +9 -44
- package/src/workflows/feature/review.ts +106 -0
- package/src/workflows/feature/steps.test.ts +82 -0
- package/src/workflows/feature/steps.ts +92 -0
- package/src/workflows/fix/fix.feature +80 -0
- package/src/workflows/fix/fix.test.ts +74 -0
- package/src/workflows/fix/fix.ts +52 -0
- package/src/workflows/fix/index.ts +6 -0
- package/src/workflows/index.ts +36 -2
- package/src/workflows/review/index.ts +6 -0
- package/src/workflows/review/review.test.ts +99 -0
- package/src/workflows/review/review.ts +33 -0
- package/src/workflows/{access.test.ts → shared/access.test.ts} +1 -1
- package/src/workflows/{access.ts → shared/access.ts} +3 -5
- package/src/workflows/shared/config.fixture.ts +12 -0
- package/src/workflows/{health.test.ts → shared/health.test.ts} +1 -1
- package/src/workflows/{health.ts → shared/health.ts} +38 -55
- package/src/workflows/{options.test.ts → shared/options.test.ts} +2 -2
- package/src/workflows/{options.ts → shared/options.ts} +14 -2
- package/src/workflows/shared/reserved.ts +11 -0
- package/src/workflows/{scenarios.test.ts → shared/scenarios.test.ts} +1 -1
- package/src/workflows/shared/scenarios.ts +35 -0
- package/src/workflows/{skills.test.ts → shared/skills.test.ts} +3 -1
- package/src/workflows/{skills.ts → shared/skills.ts} +7 -6
- package/src/workflows/{steps.test.ts → shared/steps.test.ts} +1 -66
- package/src/workflows/{steps.ts → shared/steps.ts} +8 -98
- package/src/workflows/shared/tail.test.ts +205 -0
- package/src/workflows/shared/tail.ts +135 -0
- package/src/workflows/{text.fixture.ts → shared/text.fixture.ts} +2 -2
- package/src/workflows/{text.ts → shared/text.ts} +48 -7
- package/src/workflows/{threads.test.ts → shared/threads.test.ts} +1 -1
- package/src/workflows/shared/workflows.steps.ts +7 -0
- package/src/workflows/review.ts +0 -362
- /package/src/workflows/{prose.ts → shared/prose.ts} +0 -0
- /package/src/workflows/{text.test.ts → shared/text.test.ts} +0 -0
- /package/src/workflows/{vars.ts → shared/vars.ts} +0 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coverage for `src/platform/WorktreeState.ts`'s `worktreeGitDir`/`loopLogPath`: an
|
|
3
|
+
* in-memory group pinning the `.git` resolution table (D1's own words in
|
|
4
|
+
* gtd#169), and a real-git group proving the resolver is byte-identical to
|
|
5
|
+
* `git rev-parse --git-dir` — including under a `GIT_DIR`/`GIT_WORK_TREE`
|
|
6
|
+
* pointed at an unrelated repo, the env-immunity property a driver's own
|
|
7
|
+
* `env -u` scrub is expected to provide.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { join } from "node:path"
|
|
11
|
+
import { mkdtempSync, realpathSync } from "node:fs"
|
|
12
|
+
import { execSync } from "node:child_process"
|
|
13
|
+
import { tmpdir } from "node:os"
|
|
14
|
+
import { Effect, Layer } from "effect"
|
|
15
|
+
import { NodeContext } from "@effect/platform-node"
|
|
16
|
+
import { describe, expect, it } from "vitest"
|
|
17
|
+
import { loopLogPath, worktreeGitDir } from "./WorktreeState.js"
|
|
18
|
+
import { GitService, Host, Workspace } from "./index.js"
|
|
19
|
+
import { InMemRepo, testLayers } from "../testkit/index.js"
|
|
20
|
+
|
|
21
|
+
const provide = <A>(
|
|
22
|
+
eff: Effect.Effect<A, never, Host | Workspace>,
|
|
23
|
+
repo: InMemRepo,
|
|
24
|
+
opts: {
|
|
25
|
+
readonly root?: string
|
|
26
|
+
readonly env?: Readonly<Record<string, string | undefined>>
|
|
27
|
+
} = {},
|
|
28
|
+
): Promise<A> => Effect.runPromise(eff.pipe(Effect.provide(testLayers(repo, opts))))
|
|
29
|
+
|
|
30
|
+
describe("worktreeGitDir / loopLogPath [in-memory]", () => {
|
|
31
|
+
it("falls back to .git when the worktree has no .git entry at all", async () => {
|
|
32
|
+
const repo = new InMemRepo()
|
|
33
|
+
expect(await provide(worktreeGitDir, repo)).toBe(".git")
|
|
34
|
+
expect(await provide(loopLogPath, repo)).toBe(".git/gtd-loop.log")
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("follows an absolute gitdir: pointer (a linked worktree)", async () => {
|
|
38
|
+
const repo = new InMemRepo()
|
|
39
|
+
repo.writeFile(".git", "gitdir: /abs/path\n")
|
|
40
|
+
expect(await provide(worktreeGitDir, repo)).toBe("/abs/path")
|
|
41
|
+
expect(await provide(loopLogPath, repo)).toBe(join("/abs/path", "gtd-loop.log"))
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("resolves a relative gitdir: pointer against the worktree root", async () => {
|
|
45
|
+
const repo = new InMemRepo()
|
|
46
|
+
repo.writeFile(".git", "gitdir: ../shared/worktrees/x\n")
|
|
47
|
+
expect(await provide(worktreeGitDir, repo)).toBe(join("/repo", "../shared/worktrees/x"))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("falls back to .git when the .git file has no gitdir: line", async () => {
|
|
51
|
+
const repo = new InMemRepo()
|
|
52
|
+
repo.writeFile(".git", "not a gitdir pointer\n")
|
|
53
|
+
expect(await provide(worktreeGitDir, repo)).toBe(".git")
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("GTD_LOOP_LOG wins verbatim, even over a gitdir: pointer", async () => {
|
|
57
|
+
const repo = new InMemRepo()
|
|
58
|
+
repo.writeFile(".git", "gitdir: /abs/path\n")
|
|
59
|
+
expect(
|
|
60
|
+
await provide(loopLogPath, repo, { env: { GTD_LOOP_LOG: "/elsewhere/custom.log" } }),
|
|
61
|
+
).toBe("/elsewhere/custom.log")
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("treats an empty GTD_LOOP_LOG as unset", async () => {
|
|
65
|
+
const repo = new InMemRepo()
|
|
66
|
+
expect(await provide(loopLogPath, repo, { env: { GTD_LOOP_LOG: "" } })).toBe(
|
|
67
|
+
".git/gtd-loop.log",
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it("an absolute GIT_DIR names the git dir for the log path, even over a gitdir: pointer", async () => {
|
|
72
|
+
const repo = new InMemRepo()
|
|
73
|
+
repo.writeFile(".git", "gitdir: /abs/pointer-path\n")
|
|
74
|
+
expect(await provide(loopLogPath, repo, { env: { GIT_DIR: "/elsewhere/gitdir" } })).toBe(
|
|
75
|
+
join("/elsewhere/gitdir", "gtd-loop.log"),
|
|
76
|
+
)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("a relative GIT_DIR is joined against the repo root", async () => {
|
|
80
|
+
const repo = new InMemRepo()
|
|
81
|
+
expect(
|
|
82
|
+
await provide(loopLogPath, repo, { root: "/repo", env: { GIT_DIR: "../shared/gitdir" } }),
|
|
83
|
+
).toBe(join("/repo", "../shared/gitdir", "gtd-loop.log"))
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("treats an empty GIT_DIR as unset, falling through to the pointer read", async () => {
|
|
87
|
+
const repo = new InMemRepo()
|
|
88
|
+
repo.writeFile(".git", "gitdir: /abs/pointer-path\n")
|
|
89
|
+
expect(await provide(loopLogPath, repo, { env: { GIT_DIR: "" } })).toBe(
|
|
90
|
+
join("/abs/pointer-path", "gtd-loop.log"),
|
|
91
|
+
)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it("GTD_LOOP_LOG still wins verbatim over GIT_DIR", async () => {
|
|
95
|
+
const repo = new InMemRepo()
|
|
96
|
+
expect(
|
|
97
|
+
await provide(loopLogPath, repo, {
|
|
98
|
+
env: { GTD_LOOP_LOG: "/elsewhere/custom.log", GIT_DIR: "/elsewhere/gitdir" },
|
|
99
|
+
}),
|
|
100
|
+
).toBe("/elsewhere/custom.log")
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("falls back to the literal .git when GIT_DIR is unset and the fake filesystem has no .git entry", async () => {
|
|
104
|
+
const repo = new InMemRepo()
|
|
105
|
+
expect(await provide(loopLogPath, repo)).toBe(".git/gtd-loop.log")
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe("worktreeGitDir [real git]", () => {
|
|
110
|
+
const gitExecIn = (dir: string, ...args: string[]): string =>
|
|
111
|
+
execSync(`git ${args.join(" ")}`, { cwd: dir, encoding: "utf8", stdio: "pipe" }).trim()
|
|
112
|
+
|
|
113
|
+
const runIn = (root: string) => {
|
|
114
|
+
const hostLayer = Host.layer({ root, home: root, env: {} })
|
|
115
|
+
const gitLayer = GitService.Live.pipe(Layer.provide(Layer.merge(hostLayer, NodeContext.layer)))
|
|
116
|
+
const workspaceLayer = Workspace.Live.pipe(Layer.provide(Layer.merge(hostLayer, gitLayer)))
|
|
117
|
+
return Effect.runPromise(
|
|
118
|
+
worktreeGitDir.pipe(Effect.provide(Layer.mergeAll(hostLayer, workspaceLayer))),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
it("matches git rev-parse --git-dir in the main repo and in a linked worktree", async () => {
|
|
123
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "gtd-worktree-state-")))
|
|
124
|
+
gitExecIn(root, "init", "-q")
|
|
125
|
+
gitExecIn(root, "config", "user.email", "test@test.com")
|
|
126
|
+
gitExecIn(root, "config", "user.name", "Test")
|
|
127
|
+
gitExecIn(root, "config", "commit.gpgsign", "false")
|
|
128
|
+
gitExecIn(root, "commit", "--allow-empty", "-q", "-m", "init")
|
|
129
|
+
|
|
130
|
+
expect(await runIn(root)).toBe(gitExecIn(root, "rev-parse", "--git-dir"))
|
|
131
|
+
|
|
132
|
+
const siblingDir = `${root}-sibling`
|
|
133
|
+
gitExecIn(root, "worktree", "add", "-q", "-b", "sibling", siblingDir, "HEAD")
|
|
134
|
+
expect(await runIn(siblingDir)).toBe(gitExecIn(siblingDir, "rev-parse", "--git-dir"))
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it("is immune to a GIT_DIR/GIT_WORK_TREE pointed at a third, unrelated repo", async () => {
|
|
138
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "gtd-worktree-state-")))
|
|
139
|
+
gitExecIn(root, "init", "-q")
|
|
140
|
+
gitExecIn(root, "config", "user.email", "test@test.com")
|
|
141
|
+
gitExecIn(root, "config", "user.name", "Test")
|
|
142
|
+
gitExecIn(root, "config", "commit.gpgsign", "false")
|
|
143
|
+
gitExecIn(root, "commit", "--allow-empty", "-q", "-m", "init")
|
|
144
|
+
const expected = await runIn(root)
|
|
145
|
+
|
|
146
|
+
const other = realpathSync(mkdtempSync(join(tmpdir(), "gtd-worktree-state-other-")))
|
|
147
|
+
gitExecIn(other, "init", "-q", "--bare")
|
|
148
|
+
|
|
149
|
+
const savedGitDir = process.env.GIT_DIR
|
|
150
|
+
const savedWorkTree = process.env.GIT_WORK_TREE
|
|
151
|
+
try {
|
|
152
|
+
process.env.GIT_DIR = other
|
|
153
|
+
process.env.GIT_WORK_TREE = root
|
|
154
|
+
expect(await runIn(root)).toBe(expected)
|
|
155
|
+
} finally {
|
|
156
|
+
if (savedGitDir === undefined) delete process.env.GIT_DIR
|
|
157
|
+
else process.env.GIT_DIR = savedGitDir
|
|
158
|
+
if (savedWorkTree === undefined) delete process.env.GIT_WORK_TREE
|
|
159
|
+
else process.env.GIT_WORK_TREE = savedWorkTree
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isAbsolute, join } from "node:path"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { Host } from "./Host.js"
|
|
4
|
+
import { Workspace } from "./Workspace.js"
|
|
5
|
+
|
|
6
|
+
const GITDIR_POINTER = /^gitdir:\s*(.+)$/m
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The worktree's own git dir, resolved via the filesystem rather than a `git`
|
|
10
|
+
* subprocess — byte-identical to `git rev-parse --git-dir`. Never fails: a
|
|
11
|
+
* read failure or a `.git` file with no `gitdir:` line both fall back to
|
|
12
|
+
* `".git"`, which is also correct for the in-memory test tier.
|
|
13
|
+
*/
|
|
14
|
+
export const worktreeGitDir: Effect.Effect<string, never, Host | Workspace> = Effect.gen(
|
|
15
|
+
function* () {
|
|
16
|
+
const { root } = yield* Host
|
|
17
|
+
const workspace = yield* Workspace
|
|
18
|
+
const content = yield* workspace
|
|
19
|
+
.read(".git")
|
|
20
|
+
.pipe(Effect.catchAll(() => Effect.succeed(undefined)))
|
|
21
|
+
const pointer = content === undefined ? null : GITDIR_POINTER.exec(content)
|
|
22
|
+
if (pointer === null) return ".git"
|
|
23
|
+
const gitdir = pointer[1]!.trim()
|
|
24
|
+
return isAbsolute(gitdir) ? gitdir : join(root, gitdir)
|
|
25
|
+
},
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The per-worktree loop log path `gtd next --json` reports as `log`.
|
|
30
|
+
* Precedence: `$GTD_LOOP_LOG` verbatim, then `$GIT_DIR` (so the log follows
|
|
31
|
+
* wherever git subprocesses actually write), else `worktreeGitDir`. `$GIT_DIR`
|
|
32
|
+
* is honored rather than guarded against on purpose — an inherited (not
|
|
33
|
+
* deliberately set) `GIT_DIR` will move the log path too, so a caller that
|
|
34
|
+
* doesn't want that (e.g. the e2e suite) must scrub it before invoking gtd.
|
|
35
|
+
* Advisory only — gtd neither creates nor truncates the file.
|
|
36
|
+
*/
|
|
37
|
+
export const loopLogPath: Effect.Effect<string, never, Host | Workspace> = Effect.gen(function* () {
|
|
38
|
+
const { root, env } = yield* Host
|
|
39
|
+
const override = env.GTD_LOOP_LOG
|
|
40
|
+
if (override !== undefined && override !== "") return override
|
|
41
|
+
|
|
42
|
+
const gitDirEnv = env.GIT_DIR
|
|
43
|
+
if (gitDirEnv !== undefined && gitDirEnv !== "") {
|
|
44
|
+
const gitDir = isAbsolute(gitDirEnv) ? gitDirEnv : join(root, gitDirEnv)
|
|
45
|
+
return join(gitDir, "gtd-loop.log")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const gitDir = yield* worktreeGitDir
|
|
49
|
+
return join(gitDir, "gtd-loop.log")
|
|
50
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gtd's two ports onto its environment: `Workspace` (repo file content —
|
|
3
|
+
* working-tree reads/writes plus the read-at-ref path) and `Host` (root,
|
|
4
|
+
* home, env, scratch). `GitService` moved in alongside `Workspace` since
|
|
5
|
+
* `committed` IS `git.readFileAtRef` — one module owns every read.
|
|
6
|
+
*
|
|
7
|
+
* Seam rule, stated once: a seam is a `Context.Tag` unless the consuming
|
|
8
|
+
* code is synchronous, non-Effect code that cannot `yield*` — replay's flow
|
|
9
|
+
* reads are that exception, and use the `*Sync` members.
|
|
10
|
+
*/
|
|
11
|
+
export { Workspace, type WorkspaceOps } from "./Workspace.js"
|
|
12
|
+
export { Host, type HostOps } from "./Host.js"
|
|
13
|
+
export {
|
|
14
|
+
GitService,
|
|
15
|
+
type GitOperations,
|
|
16
|
+
type GitReaderOperations,
|
|
17
|
+
type GitWriterOperations,
|
|
18
|
+
} from "./Git.js"
|
|
19
|
+
export { Narrator, GtdError, GtdUsageError, renderFailure } from "./Commentary.js"
|
|
20
|
+
export { CommandRunner } from "./CommandRunner.js"
|
|
21
|
+
export { loopLogPath } from "./WorktreeState.js"
|
|
22
|
+
export { HISTORY_REF, readRetainedHistory, restorability } from "./RetainedHistory.js"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { accessShapeFault, foldAccess } from "./Access.js"
|
|
3
|
+
|
|
4
|
+
describe("foldAccess", () => {
|
|
5
|
+
it("adds the steering file to each restricted side, after the scope globs", () => {
|
|
6
|
+
expect(foldAccess({ read: ["docs/**"], write: ["src/**"] }, ".gtd/X.md", [])).toEqual({
|
|
7
|
+
read: ["docs/**", ".gtd/X.md"],
|
|
8
|
+
write: ["src/**", ".gtd/X.md"],
|
|
9
|
+
})
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it("adds the paths of waiting code threads after the steering file", () => {
|
|
13
|
+
expect(foldAccess({ write: [] }, ".gtd/X.md", ["src/a.ts"])).toEqual({
|
|
14
|
+
read: null,
|
|
15
|
+
write: [".gtd/X.md", "src/a.ts"],
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("dedupes against scope globs and across additions", () => {
|
|
20
|
+
expect(foldAccess({ write: ["src/a.ts", ".gtd/X.md"] }, ".gtd/X.md", ["src/a.ts"])).toEqual({
|
|
21
|
+
read: null,
|
|
22
|
+
write: ["src/a.ts", ".gtd/X.md"],
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("leaves an unrestricted side null, whatever is added", () => {
|
|
27
|
+
expect(foldAccess({}, ".gtd/X.md", ["src/a.ts"])).toEqual({ read: null, write: null })
|
|
28
|
+
expect(foldAccess(undefined, ".gtd/X.md", [])).toEqual({ read: null, write: null })
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("adds nothing for a missing file", () => {
|
|
32
|
+
expect(foldAccess({ read: ["a"], write: ["b"] }, undefined, [])).toEqual({
|
|
33
|
+
read: ["a"],
|
|
34
|
+
write: ["b"],
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe("accessShapeFault", () => {
|
|
40
|
+
it("accepts {} and either side alone", () => {
|
|
41
|
+
expect(accessShapeFault({})).toBeUndefined()
|
|
42
|
+
expect(accessShapeFault({ read: [] })).toBeUndefined()
|
|
43
|
+
expect(accessShapeFault({ write: ["a/**"] })).toBeUndefined()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it("names what is wrong", () => {
|
|
47
|
+
expect(accessShapeFault("x")).toContain("must be an object")
|
|
48
|
+
expect(accessShapeFault(["a"])).toContain("must be an object")
|
|
49
|
+
expect(accessShapeFault({ write: "a" })).toContain("access.write must be an array")
|
|
50
|
+
expect(accessShapeFault({ read: [1] })).toContain("access.read must be an array")
|
|
51
|
+
expect(accessShapeFault({ exec: [] })).toContain('unknown access key "exec"')
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AccessDef, ScopeAccess } from "../flows/index.js"
|
|
2
|
+
|
|
3
|
+
const UNRESTRICTED: ScopeAccess = {}
|
|
4
|
+
|
|
5
|
+
const withExtras = (
|
|
6
|
+
globs: readonly string[] | undefined,
|
|
7
|
+
extras: readonly string[],
|
|
8
|
+
): readonly string[] | null => (globs === undefined ? null : [...new Set([...globs, ...extras])])
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Fold a step's own paths into a scope's access: each restricted side gets the
|
|
12
|
+
* steering `file`, then the code threads waiting on the agent. An unrestricted
|
|
13
|
+
* side stays `null`.
|
|
14
|
+
*/
|
|
15
|
+
export const foldAccess = (
|
|
16
|
+
scopeAccess: ScopeAccess | undefined,
|
|
17
|
+
file: string | undefined,
|
|
18
|
+
threadPaths: readonly string[],
|
|
19
|
+
): AccessDef => {
|
|
20
|
+
const access = scopeAccess ?? UNRESTRICTED
|
|
21
|
+
const extras = [...(file === undefined ? [] : [file]), ...threadPaths]
|
|
22
|
+
return { read: withExtras(access.read, extras), write: withExtras(access.write, extras) }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The shape fault of a `scope()` `access` value, or `undefined` when well-formed. */
|
|
26
|
+
export const accessShapeFault = (value: unknown): string | undefined => {
|
|
27
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
28
|
+
return "access must be an object of { read?, write? } glob arrays"
|
|
29
|
+
}
|
|
30
|
+
for (const [side, globs] of Object.entries(value)) {
|
|
31
|
+
if (side !== "read" && side !== "write") return `unknown access key "${side}"`
|
|
32
|
+
if (globs === undefined) continue
|
|
33
|
+
if (!Array.isArray(globs) || globs.some((g) => typeof g !== "string")) {
|
|
34
|
+
return `access.${side} must be an array of glob strings`
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return undefined
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const sameGlobs = (a: readonly string[] | undefined, b: readonly string[] | undefined): boolean =>
|
|
41
|
+
a === undefined || b === undefined
|
|
42
|
+
? a === b
|
|
43
|
+
: a.length === b.length && a.every((g, i) => g === b[i])
|
|
44
|
+
|
|
45
|
+
export const sameAccess = (a: ScopeAccess | undefined, b: ScopeAccess | undefined): boolean =>
|
|
46
|
+
sameGlobs(a?.read, b?.read) && sameGlobs(a?.write, b?.write)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { formatFault } from "./Format.js"
|
|
3
|
+
|
|
4
|
+
describe("the history format", () => {
|
|
5
|
+
it("accepts a history entirely in the current format", () => {
|
|
6
|
+
expect(formatFault([{ hash: "a".repeat(40), format: 1 }])).toBeUndefined()
|
|
7
|
+
expect(formatFault([])).toBeUndefined()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it("names the first foreign commit, the found and readable formats, and what to do", () => {
|
|
11
|
+
const fault = formatFault([
|
|
12
|
+
{ hash: "a".repeat(40), format: 1 },
|
|
13
|
+
{ hash: "b".repeat(40), format: 2 },
|
|
14
|
+
{ hash: "c".repeat(40), format: 3 },
|
|
15
|
+
])
|
|
16
|
+
expect(fault).toBe(
|
|
17
|
+
"gtd: commit bbbbbbb is written in history format 2, but this gtd reads format 1 — run a gtd release that reads format 2 to continue this process",
|
|
18
|
+
)
|
|
19
|
+
})
|
|
20
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Bump only when what replay reads back out of git changes incompatibly — the
|
|
2
|
+
// package version must not stand in for it, since most majors leave history
|
|
3
|
+
// untouched. A bump that keeps reading the previous format widens the check below.
|
|
4
|
+
export const HISTORY_FORMAT = 1
|
|
5
|
+
|
|
6
|
+
const short = (hash: string): string => hash.slice(0, 7)
|
|
7
|
+
|
|
8
|
+
export const formatFault = (
|
|
9
|
+
commits: readonly { readonly hash: string; readonly format: number }[],
|
|
10
|
+
): string | undefined => {
|
|
11
|
+
const foreign = commits.find((c) => c.format !== HISTORY_FORMAT)
|
|
12
|
+
if (foreign === undefined) return undefined
|
|
13
|
+
return `gtd: commit ${short(foreign.hash)} is written in history format ${foreign.format}, but this gtd reads format ${HISTORY_FORMAT} — run a gtd release that reads format ${foreign.format} to continue this process`
|
|
14
|
+
}
|