@pmelab/gtd 24.0.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 +31 -5
- 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 +43339 -55575
- 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 +8 -6
- package/package.json +69 -8
- 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/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/workflows/feature/feature.test.ts +27 -33
- package/src/workflows/feature/index.ts +10 -0
- package/src/workflows/fix/fix.feature +80 -0
- package/src/workflows/fix/fix.test.ts +58 -78
- package/src/workflows/fix/index.ts +6 -0
- package/src/workflows/index.ts +2 -10
- package/src/workflows/review/index.ts +6 -0
- package/src/workflows/review/review.test.ts +85 -29
- package/src/workflows/review/review.ts +3 -1
- package/src/workflows/shared/config.fixture.ts +12 -0
- package/src/workflows/shared/reserved.ts +11 -0
- package/src/workflows/shared/workflows.steps.ts +7 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `src/install/Install.ts` is pure string data — these tests pin its two load-bearing
|
|
3
|
+
* properties: `MINIMAL_DRIVER` stays byte-equal to docs/driver.md's own
|
|
4
|
+
* fenced driver block (the single source of truth for both), and
|
|
5
|
+
* `renderBriefing()` names every command/field a driver actually invokes, so
|
|
6
|
+
* a protocol change elsewhere can't silently drift the briefing out of sync.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from "node:fs"
|
|
9
|
+
import { resolve } from "node:path"
|
|
10
|
+
import { createRequire } from "node:module"
|
|
11
|
+
import { describe, expect, it } from "vitest"
|
|
12
|
+
import { EDIT_COMMAND, DOOR_COMMAND, MINIMAL_DRIVER, renderBriefing } from "./Install.js"
|
|
13
|
+
|
|
14
|
+
const SUITE_PATHS = ["gtd-build", "gtd-edit", "gtd-door"]
|
|
15
|
+
const SUPERSEDED_PATHS = ["gtd-fix", "gtd-review"]
|
|
16
|
+
|
|
17
|
+
const _require = createRequire(import.meta.url)
|
|
18
|
+
const GTD_VERSION: string = (_require("../../package.json") as { version: string }).version
|
|
19
|
+
|
|
20
|
+
describe("MINIMAL_DRIVER", () => {
|
|
21
|
+
it("equals docs/driver.md's own 'A complete minimal driver' fenced bash block", () => {
|
|
22
|
+
const doc = readFileSync(resolve(import.meta.dirname, "../../docs/driver.md"), "utf8")
|
|
23
|
+
const match = doc.match(/### A complete minimal driver\n[\s\S]*?```bash\n([\s\S]*?)\n```/)
|
|
24
|
+
expect(match).not.toBeNull()
|
|
25
|
+
expect(match![1]).toBe(MINIMAL_DRIVER)
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe("renderBriefing", () => {
|
|
30
|
+
it("embeds MINIMAL_DRIVER verbatim", () => {
|
|
31
|
+
expect(renderBriefing()).toContain(MINIMAL_DRIVER)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("names every command a driver invokes", () => {
|
|
35
|
+
const briefing = renderBriefing()
|
|
36
|
+
expect(briefing).toContain("gtd next --json")
|
|
37
|
+
expect(briefing).toContain("gtd next")
|
|
38
|
+
expect(briefing).toContain("gtd land")
|
|
39
|
+
expect(briefing).not.toContain("gtd status --json")
|
|
40
|
+
// The bare whole-document form is never recommended — only the
|
|
41
|
+
// `--json=<path>` selector form (e.g. `gtd land --json=script`).
|
|
42
|
+
expect(briefing).not.toMatch(/gtd land --json(?!=)/)
|
|
43
|
+
expect(briefing).not.toContain("--dispatch")
|
|
44
|
+
expect(briefing).not.toContain("--if-resting")
|
|
45
|
+
expect(briefing).not.toContain("gtd step")
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("names the version", () => {
|
|
49
|
+
expect(renderBriefing()).toContain(GTD_VERSION)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("names gtd judge answer --json=script alongside the other emitted-script commands", () => {
|
|
53
|
+
expect(renderBriefing()).toContain("gtd judge answer --json=script")
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("the ## Driver obligations section itself — not just PREREQUISITES elsewhere in the briefing — tells the driver about gtd judge answer", () => {
|
|
57
|
+
// Round 4's own finding: a grep over the WHOLE briefing already passed
|
|
58
|
+
// via PREREQUISITES/JSON_FIELD_REFERENCE, even though obligations 7/8
|
|
59
|
+
// (the numbered list a driver author actually implements against) still
|
|
60
|
+
// described gtd land as the only landing path. Scope the assertion to
|
|
61
|
+
// that one section so it can't pass on an unrelated mention again.
|
|
62
|
+
const briefing = renderBriefing()
|
|
63
|
+
const start = briefing.indexOf("## Driver obligations")
|
|
64
|
+
const end = briefing.indexOf("## Recovery")
|
|
65
|
+
expect(start).toBeGreaterThan(-1)
|
|
66
|
+
expect(end).toBeGreaterThan(start)
|
|
67
|
+
const obligations = briefing.slice(start, end)
|
|
68
|
+
expect(obligations).toContain("gtd judge answer")
|
|
69
|
+
expect(obligations).toContain("--json=judge")
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it("obligation 11 tells the driver to pass the turn's skills where the CLI can restrict them", () => {
|
|
73
|
+
const briefing = renderBriefing()
|
|
74
|
+
const obligations = briefing.slice(
|
|
75
|
+
briefing.indexOf("## Driver obligations"),
|
|
76
|
+
briefing.indexOf("## Recovery"),
|
|
77
|
+
)
|
|
78
|
+
const eleven = obligations.slice(obligations.indexOf("\n11. "))
|
|
79
|
+
expect(eleven).toContain("`pi`")
|
|
80
|
+
expect(eleven).toContain("`claude -p` cannot")
|
|
81
|
+
expect(eleven).toContain("--json=skills")
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it("obligation 12 tells the driver to map access onto the CLI's sandbox or container mounts", () => {
|
|
85
|
+
const briefing = renderBriefing()
|
|
86
|
+
const obligations = briefing.slice(
|
|
87
|
+
briefing.indexOf("## Driver obligations"),
|
|
88
|
+
briefing.indexOf("## Recovery"),
|
|
89
|
+
)
|
|
90
|
+
const twelve = obligations.slice(obligations.indexOf("\n12. "))
|
|
91
|
+
expect(twelve).toContain("--json=access.read")
|
|
92
|
+
expect(twelve).toContain("--json=access.write")
|
|
93
|
+
expect(twelve).toContain("workspace-write")
|
|
94
|
+
expect(twelve).toContain(":ro")
|
|
95
|
+
expect(twelve).toContain(":rw")
|
|
96
|
+
expect(twelve).toMatch(/cannot restrict/i)
|
|
97
|
+
expect(twelve).toMatch(/refusal/i)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it("instructs the agent to investigate the repo and ask before driving", () => {
|
|
101
|
+
expect(renderBriefing()).toMatch(/investigate the repository and ask/i)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it("points repository settings at gtd.config.ts, never a removed .gtdrc or gtd init", () => {
|
|
105
|
+
const briefing = renderBriefing()
|
|
106
|
+
expect(briefing).toMatch(/Make sure a .gtd\.config\.ts. governs the repository/)
|
|
107
|
+
expect(briefing).toContain("`config` export")
|
|
108
|
+
expect(briefing).not.toContain("gtd init")
|
|
109
|
+
expect(briefing).not.toContain(".gtdrc")
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("instructs committing the config before the first drive", () => {
|
|
113
|
+
expect(renderBriefing()).toMatch(/commit (it|the config).{0,40}before.{0,20}drive/is)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it("names the edit command's default path", () => {
|
|
117
|
+
expect(renderBriefing()).toContain("~/.local/bin/gtd-edit")
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it("names the `file` field the edit command reads", () => {
|
|
121
|
+
expect(renderBriefing()).toMatch(/`?file`?/)
|
|
122
|
+
expect(renderBriefing()).toContain("gtd next --json=file")
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it("documents that an absent field prints nothing, not the string null a jq recipe would print", () => {
|
|
126
|
+
expect(renderBriefing()).toMatch(/absent field (is silence|prints nothing)/i)
|
|
127
|
+
expect(renderBriefing()).toMatch(/`?null`?/)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it("names the .gtd/TODO.md fallback", () => {
|
|
131
|
+
expect(renderBriefing()).toContain(".gtd/TODO.md")
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it("names the initial-instruction use of the edit command", () => {
|
|
135
|
+
expect(renderBriefing()).toMatch(/initial state/i)
|
|
136
|
+
expect(renderBriefing()).toMatch(/sketch/i)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it("EDIT_COMMAND contains no jq", () => {
|
|
140
|
+
expect(EDIT_COMMAND).not.toMatch(/jq/)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it("names all three default command paths, and no longer the superseded pair as suite members", () => {
|
|
144
|
+
const briefing = renderBriefing()
|
|
145
|
+
expect(briefing).toContain("~/.local/bin/gtd-build")
|
|
146
|
+
expect(briefing).toContain("~/.local/bin/gtd-edit")
|
|
147
|
+
expect(briefing).toContain("~/.local/bin/gtd-door")
|
|
148
|
+
expect(briefing).not.toContain("~/.local/bin/gtd-review")
|
|
149
|
+
expect(briefing).not.toContain("~/.local/bin/gtd-fix")
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it("contains the door invocation, exec'ing the loop", () => {
|
|
153
|
+
const briefing = renderBriefing()
|
|
154
|
+
expect(briefing).toContain('gtd door "$@"')
|
|
155
|
+
expect(briefing).toContain("gtd doors")
|
|
156
|
+
expect(briefing).toMatch(/exec.{0,20}GTD_BUILD/s)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it("states gtd-door fix on a green suite is a no-op straight back to idle", () => {
|
|
160
|
+
expect(renderBriefing()).toMatch(/gtd-door fix.{0,80}green.{0,80}no-op.{0,40}idle/is)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it("states gtd-door review on a red baseline hands off to the loop, halting at the blocked gate", () => {
|
|
164
|
+
expect(renderBriefing()).toMatch(/gtd-door review.{0,120}red.{0,120}blocked gate/is)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it("states the door command execs the RESOLVED loop path, not the literal string gtd-build", () => {
|
|
168
|
+
expect(renderBriefing()).toMatch(/RESOLVED.{0,60}gtd-build.{0,80}never the literal string/is)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it("names plannerModel/coderModel, their defaults, and states they are opaque hints", () => {
|
|
172
|
+
const briefing = renderBriefing()
|
|
173
|
+
expect(briefing).toContain("plannerModel")
|
|
174
|
+
expect(briefing).toContain("coderModel")
|
|
175
|
+
expect(briefing).toContain("smart")
|
|
176
|
+
expect(briefing).toContain("base")
|
|
177
|
+
expect(briefing).toMatch(/opaque hints, not model names/i)
|
|
178
|
+
expect(briefing).toMatch(/--model smart.{0,20}fails/is)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it("names GTD_PLANNERMODEL/GTD_CODERMODEL exports, wrapped in gtd-install markers, not written to gtd.config.ts", () => {
|
|
182
|
+
const briefing = renderBriefing()
|
|
183
|
+
expect(briefing).toContain("GTD_PLANNERMODEL")
|
|
184
|
+
expect(briefing).toContain("GTD_CODERMODEL")
|
|
185
|
+
expect(briefing).toContain("# gtd-install: model exports")
|
|
186
|
+
expect(briefing).toContain("# gtd-install: end")
|
|
187
|
+
expect(briefing).toMatch(/NOT into.{0,20}gtd\.config\.ts/is)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it("states gtd-door inherits exports only via exec, and GTD_* is highest precedence", () => {
|
|
191
|
+
const briefing = renderBriefing()
|
|
192
|
+
expect(briefing).toMatch(/gtd-door.{0,20}inherits.{0,60}exports only because it.{0,10}exec/is)
|
|
193
|
+
expect(briefing).toMatch(/GTD_\*.{0,40}highest-precedence/is)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it("states the consequence of not honoring model hints", () => {
|
|
197
|
+
expect(renderBriefing()).toMatch(
|
|
198
|
+
/drop.{0,20}--model.{0,80}no.{0,20}exports.{0,60}default tier/is,
|
|
199
|
+
)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it("probes for at least two known coding-agent CLIs on PATH", () => {
|
|
203
|
+
const briefing = renderBriefing()
|
|
204
|
+
const probed = ["claude", "codex", "gemini", "cursor-agent", "aider", "opencode", "amp"]
|
|
205
|
+
const found = probed.filter((name) => briefing.includes(name))
|
|
206
|
+
expect(found.length).toBeGreaterThanOrEqual(2)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it("contains no hardcoded table of concrete model identifiers", () => {
|
|
210
|
+
const briefing = renderBriefing()
|
|
211
|
+
expect(briefing).not.toMatch(/opus|sonnet|haiku|gpt-\d|gemini-\d|o1-|o3-/i)
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
describe("REINSTALL", () => {
|
|
216
|
+
it("comes after all three command subsections, and before PREREQUISITES", () => {
|
|
217
|
+
const briefing = renderBriefing()
|
|
218
|
+
const fixIndex = briefing.lastIndexOf("### `gtd-door")
|
|
219
|
+
const reinstallIndex = briefing.indexOf("read each of the three suite paths")
|
|
220
|
+
const prereqIndex = briefing.indexOf("## Prerequisites and portability")
|
|
221
|
+
expect(fixIndex).toBeGreaterThan(-1)
|
|
222
|
+
expect(reinstallIndex).toBeGreaterThan(fixIndex)
|
|
223
|
+
expect(prereqIndex).toBeGreaterThan(reinstallIndex)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it("instructs reading each of the three suite paths before writing anything", () => {
|
|
227
|
+
const briefing = renderBriefing()
|
|
228
|
+
expect(briefing).toMatch(/read each of the three suite paths.{0,40}before writing/is)
|
|
229
|
+
for (const name of SUITE_PATHS) expect(briefing).toContain(name)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it("states the three-way branch: absent, content-equal, and different", () => {
|
|
233
|
+
const briefing = renderBriefing()
|
|
234
|
+
expect(briefing).toMatch(/absent.{0,60}install it/is)
|
|
235
|
+
expect(briefing).toMatch(/content-equal.{0,80}(change nothing|skip)/is)
|
|
236
|
+
expect(briefing).toMatch(/skip.{0,40}interview questions/is)
|
|
237
|
+
expect(briefing).toMatch(/different.{0,120}(show the difference|diff)/is)
|
|
238
|
+
expect(briefing).toMatch(/ask before overwriting/i)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it("states drift is detected by comparing content, never by parsing a version", () => {
|
|
242
|
+
const briefing = renderBriefing()
|
|
243
|
+
expect(briefing).toMatch(/comparing content/i)
|
|
244
|
+
expect(briefing).toMatch(/never by parsing a version/i)
|
|
245
|
+
expect(briefing).toMatch(/carries no version marker/i)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it("states an unreadable path or a directory is reported and asked about, never overwritten", () => {
|
|
249
|
+
const briefing = renderBriefing()
|
|
250
|
+
expect(briefing).toMatch(/unreadable.{0,80}directory.{0,120}never overwritten/is)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it("names the model-export markers as the region to strip before comparing gtd-build", () => {
|
|
254
|
+
const briefing = renderBriefing()
|
|
255
|
+
expect(briefing).toMatch(
|
|
256
|
+
/# gtd-install: model exports[\s\S]{0,120}# gtd-install: end.{0,120}strip/i,
|
|
257
|
+
)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it("states a gtd-build differing only inside the markers is unchanged, and re-asks nothing", () => {
|
|
261
|
+
const briefing = renderBriefing()
|
|
262
|
+
expect(briefing).toMatch(/differing only.{0,60}(inside|within).{0,60}(markers|exports)/is)
|
|
263
|
+
expect(briefing).toMatch(/unchanged.{0,40}re-asks? nothing/is)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it("states why: resolved model names are per machine, so every re-install would report drift otherwise", () => {
|
|
267
|
+
const briefing = renderBriefing()
|
|
268
|
+
expect(briefing).toMatch(/per machine/i)
|
|
269
|
+
expect(briefing).toMatch(/every.{0,20}re-install.{0,40}report(s|ing)? drift/is)
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it("scopes the check to exactly the three suite paths, plus the superseded pair", () => {
|
|
273
|
+
const briefing = renderBriefing()
|
|
274
|
+
expect(briefing).toMatch(/exactly the three suite paths/i)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it("names gtd-fix and gtd-review as superseded by gtd-door, and asks before removing them", () => {
|
|
278
|
+
const collapsed = renderBriefing().replace(/\s+/g, " ")
|
|
279
|
+
for (const name of SUPERSEDED_PATHS) expect(collapsed).toContain(`\`${name}\``)
|
|
280
|
+
expect(collapsed).toMatch(/superseded by `gtd-door`/)
|
|
281
|
+
expect(collapsed).toMatch(/ask before removing it/i)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it("never names gtd-loop as something to read, diff, delete, or remove", () => {
|
|
285
|
+
// Scoped to the sentences that actually mention "gtd-loop", found in the
|
|
286
|
+
// whitespace-collapsed briefing (not per wrapped line — the briefing is
|
|
287
|
+
// hard-wrapped prose, so a line-by-line filter can land "gtd-loop" and a
|
|
288
|
+
// removal verb on different lines and miss the very sentence it exists
|
|
289
|
+
// to check). Scoping to those sentences (rather than the whole document)
|
|
290
|
+
// avoids false positives from unrelated uses of "diff" elsewhere, e.g.
|
|
291
|
+
// the driver protocol's own "a full diff" language.
|
|
292
|
+
const collapsed = renderBriefing().replace(/\s+/g, " ")
|
|
293
|
+
const loopSentences = collapsed
|
|
294
|
+
.split(/(?<=[.!?])\s+/)
|
|
295
|
+
.filter((sentence) => sentence.includes("gtd-loop"))
|
|
296
|
+
expect(loopSentences.length).toBeGreaterThan(0)
|
|
297
|
+
for (const sentence of loopSentences) {
|
|
298
|
+
expect(sentence).not.toMatch(/\b(remove|delete|diff)\b/i)
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it("states an existing gtd-loop survives untouched, and cleanup is the human's own call", () => {
|
|
303
|
+
const briefing = renderBriefing()
|
|
304
|
+
expect(briefing).toMatch(/gtd-loop.{0,60}survives untouched/is)
|
|
305
|
+
expect(briefing).toMatch(/human's own call/i)
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
describe("EDITOR_INTEGRATION", () => {
|
|
310
|
+
it("comes after the command suite and before PREREQUISITES", () => {
|
|
311
|
+
const briefing = renderBriefing()
|
|
312
|
+
const fixIndex = briefing.lastIndexOf("### `gtd-door")
|
|
313
|
+
const editorIndex = briefing.indexOf("## Editor integration")
|
|
314
|
+
const prereqIndex = briefing.indexOf("## Prerequisites and portability")
|
|
315
|
+
expect(fixIndex).toBeGreaterThan(-1)
|
|
316
|
+
expect(editorIndex).toBeGreaterThan(fixIndex)
|
|
317
|
+
expect(prereqIndex).toBeGreaterThan(editorIndex)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it("names gtd lsp and stdio as the integration contract", () => {
|
|
321
|
+
const briefing = renderBriefing()
|
|
322
|
+
expect(briefing).toContain("gtd lsp")
|
|
323
|
+
expect(briefing).toMatch(/\bstdio\b/i)
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it("names no specific editor and carries no per-editor recipe", () => {
|
|
327
|
+
const briefing = renderBriefing()
|
|
328
|
+
for (const editor of ["VS Code", "Neovim", "Helix", "Zed", "Emacs"]) {
|
|
329
|
+
expect(briefing).not.toContain(editor)
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it("instructs reading $EDITOR/$VISUAL first, then shell rc files, for the user's editor", () => {
|
|
334
|
+
const briefing = renderBriefing()
|
|
335
|
+
expect(briefing).toContain("$EDITOR")
|
|
336
|
+
expect(briefing).toContain("$VISUAL")
|
|
337
|
+
for (const rc of [".zshrc", ".bashrc", ".config/fish/config.fish"]) {
|
|
338
|
+
expect(briefing).toContain(rc)
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
it("instructs asking the user when detection finds nothing or more than one", () => {
|
|
343
|
+
const briefing = renderBriefing()
|
|
344
|
+
expect(briefing).toMatch(/ask\s+the\s+user\s+outright.{0,60}(nothing|more than one)/is)
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it("instructs saying nothing and moving on when no editor is found at all", () => {
|
|
348
|
+
const briefing = renderBriefing()
|
|
349
|
+
expect(briefing).toMatch(/say\s+nothing\s+and\s+move\s+on/i)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it("instructs looking the chosen editor's own LSP configuration format up", () => {
|
|
353
|
+
const briefing = renderBriefing()
|
|
354
|
+
expect(briefing).toMatch(/look.{0,20}(its|their) own LSP configuration format up/is)
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it("names gtd.openSteeringFile and both fresh-repo facts", () => {
|
|
358
|
+
const briefing = renderBriefing()
|
|
359
|
+
expect(briefing).toContain("gtd.openSteeringFile")
|
|
360
|
+
expect(briefing).toMatch(/gtd lsp.{0,40}never creates.{0,20}\.gtd\//is)
|
|
361
|
+
expect(briefing).toMatch(/gtd lsp.{0,40}needs no repository root/is)
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it("instructs editing the editor's own config file, not printing a snippet and walking away", () => {
|
|
365
|
+
const briefing = renderBriefing()
|
|
366
|
+
expect(briefing).toMatch(/edit the editor's own config file/i)
|
|
367
|
+
expect(briefing).toMatch(/never\s+print\s+a\s+snippet\s+and\s+walk\s+away/i)
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
it("instructs asking first, per editor, naming the exact file about to change", () => {
|
|
371
|
+
const briefing = renderBriefing()
|
|
372
|
+
expect(briefing).toMatch(/ask first, per editor.{0,60}naming the exact file/is)
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it("instructs merging rather than overwriting the editor config", () => {
|
|
376
|
+
const briefing = renderBriefing()
|
|
377
|
+
expect(briefing).toMatch(/merge, never overwrite/i)
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it("instructs skipping and reporting when the entry is already present", () => {
|
|
381
|
+
const briefing = renderBriefing()
|
|
382
|
+
expect(briefing).toMatch(/skip and report when the entry is already present/i)
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it("treats a malformed existing config as stop-and-report, not a rewrite", () => {
|
|
386
|
+
const briefing = renderBriefing()
|
|
387
|
+
expect(briefing).toMatch(/malformed existing.{0,40}config.{0,120}stop-and-report/is)
|
|
388
|
+
expect(briefing).toMatch(/leave it untouched/i)
|
|
389
|
+
})
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
describe("DOOR_COMMAND", () => {
|
|
393
|
+
it("is POSIX sh with no jq or bashisms", () => {
|
|
394
|
+
expect(DOOR_COMMAND).toMatch(/^#!\/usr\/bin\/env sh\n/)
|
|
395
|
+
expect(DOOR_COMMAND).toContain("set -eu")
|
|
396
|
+
expect(DOOR_COMMAND).not.toMatch(/jq/)
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
it("carries GTD_BUILD at top and execs it as the last line", () => {
|
|
400
|
+
expect(DOOR_COMMAND).toMatch(
|
|
401
|
+
/^#!\/usr\/bin\/env sh\nset -eu\nGTD_BUILD=~\/\.local\/bin\/gtd-build\n/,
|
|
402
|
+
)
|
|
403
|
+
expect(DOOR_COMMAND.trimEnd().endsWith('exec "$GTD_BUILD"')).toBe(true)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it("cds to the repository root before any gtd call", () => {
|
|
407
|
+
const cdIndex = DOOR_COMMAND.indexOf('cd "$(git rev-parse --show-toplevel)"')
|
|
408
|
+
const gtdIndex = DOOR_COMMAND.indexOf("gtd door")
|
|
409
|
+
expect(cdIndex).toBeGreaterThan(-1)
|
|
410
|
+
expect(gtdIndex).toBeGreaterThan(cdIndex)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
it("hands every argument to gtd door", () => {
|
|
414
|
+
expect(DOOR_COMMAND).toContain('gtd door "$@"')
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it("captures the emitted script via command substitution, never a pipe into sh", () => {
|
|
418
|
+
expect(DOOR_COMMAND).toContain('script="$(gtd door "$@")"')
|
|
419
|
+
expect(DOOR_COMMAND).toContain('sh -c "$script"')
|
|
420
|
+
expect(DOOR_COMMAND).not.toMatch(/gtd door[^\n]*\|\s*sh/)
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it("prints usage to stderr and exits 2 when no name is given", () => {
|
|
424
|
+
expect(DOOR_COMMAND).toMatch(/usage: gtd-door <name> \[args\.\.\.\]" >&2\n\s*exit 2/)
|
|
425
|
+
})
|
|
426
|
+
})
|