@pmelab/gtd 58.2.0 → 59.0.1
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 +120 -395
- package/dist/chunks/{Schema-B3o6eRNU.mjs → testkit-07Z_ENch.mjs} +13503 -3460
- package/dist/chunks/{wire-C12dahrH.mjs → wire-BTUhRGXj.mjs} +1 -1
- package/dist/flows.mjs +1 -1
- package/dist/gtd.bundle.mjs +484 -497
- package/dist/testkit/gherkin.mjs +145 -65
- package/dist/testkit.mjs +1 -1
- package/dist/wire.mjs +1 -1
- package/package.json +3 -42
- package/skills/authoring/SKILL.md +34 -27
- package/src/cli/Cli.test.ts +148 -71
- package/src/cli/Cli.ts +384 -463
- package/src/cli/Help.test.ts +47 -0
- package/src/cli/Help.ts +70 -0
- package/src/cli/Program.test.ts +127 -33
- package/src/cli/Program.ts +70 -25
- package/src/edge/Edge.ts +4 -4
- package/src/emit/Emit.test.ts +1 -1
- package/src/emit/Emit.ts +1 -1
- package/src/flags/index.test.ts +0 -13
- package/src/flags/index.ts +1 -3
- package/src/flows/factory.ts +3 -1
- package/src/gherkin/index.ts +135 -71
- package/src/gherkin/messages.test.ts +90 -0
- package/src/gherkin/messages.ts +84 -0
- package/src/install/Install.test.ts +14 -11
- package/src/install/Install.ts +38 -15
- package/src/install/index.ts +1 -2
- package/src/replay/Replay.test.ts +1 -1
- package/src/replay/index.ts +1 -1
- package/src/step/planStart.ts +6 -1
- package/src/testkit/TestRepo.test.ts +47 -2
- package/src/testkit/TestRepo.ts +51 -6
- package/src/twins/EmittedScriptRecognizer.ts +5 -8
- package/src/twins/FlowScripts.ts +1 -1
- package/src/wire/Land.ts +1 -1
- package/src/workflow/index.ts +1 -0
- package/src/workflow/load.test.ts +138 -113
- package/src/workflow/load.ts +7 -7
- package/src/workflow/skills.test.ts +5 -5
- package/src/workflow/skills.ts +1 -1
- package/src/workflow/vars.test.ts +30 -3
- package/src/workflow/vars.ts +34 -2
- package/src/workflow-kit/index.ts +1 -1
- package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
- package/dist/chunks/review-Bfaror38.mjs +0 -427
- package/dist/chunks/tail-Bd7L940p.mjs +0 -355
- package/dist/chunks/testkit-JDtpNKyi.mjs +0 -10410
- package/dist/workflows/cleanup.mjs +0 -36
- package/dist/workflows/feature.mjs +0 -1018
- package/dist/workflows/fix.mjs +0 -41
- package/dist/workflows/review.mjs +0 -45
- package/dist/workflows/rollout.mjs +0 -48
- package/dist/workflows.mjs +0 -40651
- package/src/install/ConfigScaffold.ts +0 -22
- package/src/workflows/cleanup/cleanup-path.feature +0 -94
- package/src/workflows/cleanup/cleanup.feature +0 -315
- package/src/workflows/cleanup/cleanup.test.ts +0 -276
- package/src/workflows/cleanup/cleanup.ts +0 -52
- package/src/workflows/cleanup/index.ts +0 -6
- package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
- package/src/workflows/feature/acceptance-criteria.feature +0 -389
- package/src/workflows/feature/criteria-coverage.feature +0 -102
- package/src/workflows/feature/criteria.test.ts +0 -147
- package/src/workflows/feature/criteria.ts +0 -267
- package/src/workflows/feature/fast-path.feature +0 -93
- package/src/workflows/feature/feature.test.ts +0 -338
- package/src/workflows/feature/feature.ts +0 -156
- package/src/workflows/feature/green.ts +0 -19
- package/src/workflows/feature/index.ts +0 -18
- package/src/workflows/feature/lens-models.feature +0 -69
- package/src/workflows/feature/lens-review.feature +0 -566
- package/src/workflows/feature/naive-acceptance.feature +0 -147
- package/src/workflows/feature/naive-test.feature +0 -263
- package/src/workflows/feature/one-step-land.feature +0 -93
- package/src/workflows/feature/planning.ts +0 -41
- package/src/workflows/feature/refactor-absorbs.feature +0 -131
- package/src/workflows/feature/refactor-first.feature +0 -158
- package/src/workflows/feature/refactor-lens-models.feature +0 -79
- package/src/workflows/feature/refactor-rebuild.feature +0 -273
- package/src/workflows/feature/refactor-review.feature +0 -1528
- package/src/workflows/feature/refactor.test.ts +0 -1220
- package/src/workflows/feature/refactor.ts +0 -368
- package/src/workflows/feature/release-flag.feature +0 -167
- package/src/workflows/feature/release-flag.test.ts +0 -49
- package/src/workflows/feature/release-flag.ts +0 -46
- package/src/workflows/feature/review-gate.feature +0 -241
- package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
- package/src/workflows/feature/risk-tier.feature +0 -351
- package/src/workflows/feature/seam-rejection.feature +0 -146
- package/src/workflows/feature/seam.feature +0 -297
- package/src/workflows/feature/seam.test.ts +0 -527
- package/src/workflows/feature/seam.ts +0 -426
- package/src/workflows/feature/standards.feature +0 -354
- package/src/workflows/feature/steps.test.ts +0 -65
- package/src/workflows/feature/steps.ts +0 -45
- package/src/workflows/feature/tier-pin.feature +0 -66
- package/src/workflows/feature/tier.test.ts +0 -166
- package/src/workflows/feature/tier.ts +0 -56
- package/src/workflows/fix/fix-lens-origin.feature +0 -114
- package/src/workflows/fix/fix-quality.feature +0 -212
- package/src/workflows/fix/fix.feature +0 -79
- package/src/workflows/fix/fix.test.ts +0 -74
- package/src/workflows/fix/fix.ts +0 -52
- package/src/workflows/fix/index.ts +0 -6
- package/src/workflows/index.ts +0 -47
- package/src/workflows/review/index.ts +0 -6
- package/src/workflows/review/review-door.feature +0 -199
- package/src/workflows/review/review.test.ts +0 -27
- package/src/workflows/review/review.ts +0 -60
- package/src/workflows/rollout/index.ts +0 -6
- package/src/workflows/rollout/rollout.feature +0 -510
- package/src/workflows/rollout/rollout.test.ts +0 -263
- package/src/workflows/rollout/rollout.ts +0 -82
- package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
- package/src/workflows/shared/access.test.ts +0 -62
- package/src/workflows/shared/access.ts +0 -24
- package/src/workflows/shared/config.fixture.ts +0 -12
- package/src/workflows/shared/fences.ts +0 -24
- package/src/workflows/shared/health.test.ts +0 -148
- package/src/workflows/shared/health.ts +0 -191
- package/src/workflows/shared/learn.test.ts +0 -140
- package/src/workflows/shared/learn.ts +0 -201
- package/src/workflows/shared/lenses.test.ts +0 -39
- package/src/workflows/shared/lenses.ts +0 -294
- package/src/workflows/shared/options.test.ts +0 -157
- package/src/workflows/shared/options.ts +0 -92
- package/src/workflows/shared/progress.feature +0 -124
- package/src/workflows/shared/prose.ts +0 -263
- package/src/workflows/shared/recheck.feature +0 -60
- package/src/workflows/shared/registry.test.ts +0 -345
- package/src/workflows/shared/registry.ts +0 -183
- package/src/workflows/shared/reserved.ts +0 -11
- package/src/workflows/shared/review.test.ts +0 -1062
- package/src/workflows/shared/review.ts +0 -446
- package/src/workflows/shared/skills.test.ts +0 -121
- package/src/workflows/shared/skills.ts +0 -49
- package/src/workflows/shared/standards.test.ts +0 -90
- package/src/workflows/shared/standards.ts +0 -156
- package/src/workflows/shared/steps.test.ts +0 -219
- package/src/workflows/shared/steps.ts +0 -201
- package/src/workflows/shared/tail.test.ts +0 -454
- package/src/workflows/shared/tail.ts +0 -264
- package/src/workflows/shared/text.fixture.ts +0 -128
- package/src/workflows/shared/text.test.ts +0 -1430
- package/src/workflows/shared/text.ts +0 -1700
- package/src/workflows/shared/threads.test.ts +0 -129
- package/src/workflows/shared/vars.ts +0 -47
- package/src/workflows/shared/workflows.steps.ts +0 -54
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { closest, definition, wrap } from "./Help.js"
|
|
3
|
+
|
|
4
|
+
describe("wrap", () => {
|
|
5
|
+
it("fills lines up to the width, never past it", () => {
|
|
6
|
+
expect(wrap("aaa bbb ccc ddd", 7)).toEqual(["aaa bbb", "ccc ddd"])
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it("keeps a code span on one line", () => {
|
|
10
|
+
expect(wrap("run `gtd help land` now", 12)).toEqual(["run", "`gtd help land`", "now"])
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it("keeps an indented example paragraph verbatim", () => {
|
|
14
|
+
expect(wrap('text\n\n s=$(x) && sh -c "$s"', 4)).toEqual([
|
|
15
|
+
"text",
|
|
16
|
+
"",
|
|
17
|
+
' s=$(x) && sh -c "$s"',
|
|
18
|
+
])
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe("definition", () => {
|
|
23
|
+
it("puts a short term beside its description", () => {
|
|
24
|
+
expect(definition("next", "Print it", 10)).toEqual([" next Print it"])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("gives a term too wide for the column a line of its own", () => {
|
|
28
|
+
expect(definition("a-very-long-term", "Text", 10)).toEqual([
|
|
29
|
+
" a-very-long-term",
|
|
30
|
+
" Text",
|
|
31
|
+
])
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe("closest", () => {
|
|
36
|
+
const commands = ["next", "land", "history", "help"]
|
|
37
|
+
|
|
38
|
+
it("finds a dropped letter and a swapped pair", () => {
|
|
39
|
+
expect(closest("nxt", commands)).toBe("next")
|
|
40
|
+
expect(closest("lnad", commands)).toBe("land")
|
|
41
|
+
expect(closest("histroy", commands)).toBe("history")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("suggests nothing for a word unlike every candidate", () => {
|
|
45
|
+
expect(closest("frobnicate", commands)).toBeUndefined()
|
|
46
|
+
})
|
|
47
|
+
})
|
package/src/cli/Help.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const HELP_WIDTH = 80
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Word-wraps prose to `width` columns, every line prefixed by `indent`.
|
|
5
|
+
* Paragraphs are separated by a blank line; a paragraph whose lines start
|
|
6
|
+
* with two spaces is an example and is kept verbatim (indented, not wrapped).
|
|
7
|
+
*/
|
|
8
|
+
const wrapParagraph = (paragraph: string, width: number, indent: string): string[] => {
|
|
9
|
+
const lines: string[] = []
|
|
10
|
+
let line = ""
|
|
11
|
+
// A `code span` is one word: a command split across lines can't be copied.
|
|
12
|
+
for (const word of paragraph.match(/\S*`[^`]*`\S*|\S+/g) ?? []) {
|
|
13
|
+
if (line !== "" && indent.length + line.length + 1 + word.length > width) {
|
|
14
|
+
lines.push(indent + line)
|
|
15
|
+
line = word
|
|
16
|
+
} else {
|
|
17
|
+
line = line === "" ? word : `${line} ${word}`
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (line !== "") lines.push(indent + line)
|
|
21
|
+
return lines
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const wrap = (text: string, width = HELP_WIDTH, indent = ""): string[] =>
|
|
25
|
+
text
|
|
26
|
+
.split("\n\n")
|
|
27
|
+
.flatMap((paragraph, i) => [
|
|
28
|
+
...(i > 0 ? [""] : []),
|
|
29
|
+
...(paragraph.startsWith(" ")
|
|
30
|
+
? paragraph.split("\n").map((l) => indent + l)
|
|
31
|
+
: wrapParagraph(paragraph, width, indent)),
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
/** `term` and `description` side by side, the description wrapped into the column after `column`; a term too wide for it gets a line of its own. */
|
|
35
|
+
export const definition = (term: string, description: string, column: number): string[] => {
|
|
36
|
+
const cell = ` ${term}`
|
|
37
|
+
const body = wrap(description, HELP_WIDTH, " ".repeat(column))
|
|
38
|
+
if (cell.length + 1 >= column) return [cell, ...body]
|
|
39
|
+
const [first = "", ...rest] = body
|
|
40
|
+
return [cell.padEnd(column) + first.trimStart(), ...rest]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Optimal-string-alignment distance: Levenshtein plus adjacent transpositions, so `nxet` is one edit from `next`. */
|
|
44
|
+
const distance = (a: string, b: string): number => {
|
|
45
|
+
const d: number[][] = Array.from({ length: a.length + 1 }, (_, i) =>
|
|
46
|
+
Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)),
|
|
47
|
+
)
|
|
48
|
+
for (let i = 1; i <= a.length; i++) {
|
|
49
|
+
for (let j = 1; j <= b.length; j++) {
|
|
50
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1
|
|
51
|
+
let best = Math.min(d[i - 1]![j]! + 1, d[i]![j - 1]! + 1, d[i - 1]![j - 1]! + cost)
|
|
52
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
53
|
+
best = Math.min(best, d[i - 2]![j - 2]! + 1)
|
|
54
|
+
}
|
|
55
|
+
d[i]![j] = best
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return d[a.length]![b.length]!
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The candidate closest to `word`, when it is a plausible typo of it: at most one edit per three characters, and at least one. */
|
|
62
|
+
export const closest = (word: string, candidates: readonly string[]): string | undefined => {
|
|
63
|
+
const limit = Math.max(1, Math.floor(word.length / 3))
|
|
64
|
+
let best: { readonly name: string; readonly d: number } | undefined
|
|
65
|
+
for (const name of candidates) {
|
|
66
|
+
const d = distance(word, name)
|
|
67
|
+
if (d <= limit && (best === undefined || d < best.d)) best = { name, d }
|
|
68
|
+
}
|
|
69
|
+
return best?.name
|
|
70
|
+
}
|
package/src/cli/Program.test.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* e2e tier observes. Argv parsing is pinned in `src/cli/Cli.test.ts`.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { Cause, Effect, Exit } from "effect"
|
|
7
|
+
import { Cause, Context, Effect, Exit } from "effect"
|
|
8
8
|
import { PassThrough } from "node:stream"
|
|
9
9
|
import { afterEach, describe, expect, it, vi } from "vitest"
|
|
10
10
|
|
|
@@ -17,7 +17,7 @@ vi.mock("node:os", async (importOriginal) => ({
|
|
|
17
17
|
|
|
18
18
|
import { runCli, type Command } from "./index.js"
|
|
19
19
|
import { EXIT_USAGE_ERROR, stallDiagnosis, noopText, WIRE_SCHEMA } from "../wire/index.js"
|
|
20
|
-
import { needsOf, runCommand, SelectorUsageError } from "./Program.js"
|
|
20
|
+
import { needsOf, runCommand, SelectorUsageError, type CommandRequirements } from "./Program.js"
|
|
21
21
|
import { InMemRepo, makeCapturingCliIo, testLayers, applyEmittedScript } from "../twins/index.js"
|
|
22
22
|
import {
|
|
23
23
|
commitAll,
|
|
@@ -26,9 +26,9 @@ import {
|
|
|
26
26
|
noteOutcome,
|
|
27
27
|
restoredOutcome,
|
|
28
28
|
} from "../emit/index.js"
|
|
29
|
-
import { HISTORY_REF } from "../platform/index.js"
|
|
30
|
-
import { CONFIG_SCAFFOLD } from "../install/index.js"
|
|
29
|
+
import { GitService, HISTORY_REF, Host } from "../platform/index.js"
|
|
31
30
|
import { contentHash } from "../workflow/index.js"
|
|
31
|
+
import { WORKFLOWS_REPO } from "../install/index.js"
|
|
32
32
|
|
|
33
33
|
const run = async (
|
|
34
34
|
repo: InMemRepo,
|
|
@@ -64,15 +64,6 @@ const seed = (source: string, extra: Record<string, string> = {}): InMemRepo =>
|
|
|
64
64
|
return repo
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
/** A repo `gtd install` set up: its config runs the shipped workflows. */
|
|
68
|
-
const seedBundled = (): InMemRepo => {
|
|
69
|
-
const repo = new InMemRepo()
|
|
70
|
-
repo.writeFile("README.md", "# test project\n")
|
|
71
|
-
repo.writeFile("gtd.config.ts", CONFIG_SCAFFOLD)
|
|
72
|
-
repo.seedCommit("chore: init")
|
|
73
|
-
return repo
|
|
74
|
-
}
|
|
75
|
-
|
|
76
67
|
const IDLE_THEN_WORKING = `import { agent, human } from "@pmelab/gtd/flows"
|
|
77
68
|
|
|
78
69
|
export default async () => {
|
|
@@ -81,6 +72,15 @@ export default async () => {
|
|
|
81
72
|
}
|
|
82
73
|
`
|
|
83
74
|
|
|
75
|
+
/** A repo at rest: its config runs {@link IDLE_THEN_WORKING}. */
|
|
76
|
+
const seedIdle = (): InMemRepo => {
|
|
77
|
+
const repo = new InMemRepo()
|
|
78
|
+
repo.writeFile("README.md", "# test project\n")
|
|
79
|
+
repo.writeFile("gtd.config.ts", IDLE_THEN_WORKING)
|
|
80
|
+
repo.seedCommit("chore: init")
|
|
81
|
+
return repo
|
|
82
|
+
}
|
|
83
|
+
|
|
84
84
|
describe("gtd --workflow <name> — a custom workflow's named workflow", () => {
|
|
85
85
|
const WORKFLOW = `import { agent, human } from "@pmelab/gtd/flows"
|
|
86
86
|
|
|
@@ -119,6 +119,19 @@ export const defaults = { greeting: "hello" }
|
|
|
119
119
|
expect(stderr).toContain("startable: hotfix")
|
|
120
120
|
})
|
|
121
121
|
|
|
122
|
+
it("lists the paths its script commits as comments a path cannot break out of", async () => {
|
|
123
|
+
const repo = seed(WORKFLOW)
|
|
124
|
+
repo.writeFile("NOTE.md", "a note\n")
|
|
125
|
+
repo.writeFile("evil\nrm -rf x", "")
|
|
126
|
+
const { stdout, exitCode } = await run(repo, "--workflow", "hotfix")
|
|
127
|
+
expect(exitCode).toBe(0)
|
|
128
|
+
expect(stdout).toContain("# commits every change in the working tree (git add -A):\n")
|
|
129
|
+
expect(stdout).toContain("# A NOTE.md\n")
|
|
130
|
+
expect(stdout).toContain("evil?rm -rf x")
|
|
131
|
+
expect(stdout).not.toMatch(/^rm -rf x/m)
|
|
132
|
+
expect(applyEmittedScript(repo, new Map(), stdout).ok).toBe(true)
|
|
133
|
+
})
|
|
134
|
+
|
|
122
135
|
it("`default` is not startable by name", async () => {
|
|
123
136
|
const repo = seed(WORKFLOW)
|
|
124
137
|
const { exitCode, stderr } = await run(repo, "--workflow", "default")
|
|
@@ -194,9 +207,50 @@ export const defaults = { greeting: "hello" }
|
|
|
194
207
|
})
|
|
195
208
|
})
|
|
196
209
|
|
|
197
|
-
describe("gtd
|
|
210
|
+
describe("gtd install", () => {
|
|
211
|
+
it("where no gtd.config.ts governs, prints the briefing and warns where to get a workflow — writing nothing", async () => {
|
|
212
|
+
const repo = new InMemRepo()
|
|
213
|
+
repo.writeFile("README.md", "# test project\n")
|
|
214
|
+
repo.seedCommit("chore: init")
|
|
215
|
+
const before = repo.snapshot()
|
|
216
|
+
const { stdout, stderr, exitCode } = await run(repo, "install")
|
|
217
|
+
expect(exitCode).toBe(0)
|
|
218
|
+
expect(stdout).toMatch(/Make sure a .gtd\.config\.ts. governs the repository/)
|
|
219
|
+
expect(stderr).toContain(
|
|
220
|
+
"gtd install: no gtd.config.ts governs here, and gtd ships no workflow",
|
|
221
|
+
)
|
|
222
|
+
expect(stderr).toContain(WORKFLOWS_REPO)
|
|
223
|
+
expect(repo.hasPath("gtd.config.ts")).toBe(false)
|
|
224
|
+
expect(repo.snapshot()).toBe(before)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it("where a gtd.config.ts governs, prints the briefing alone", async () => {
|
|
228
|
+
const repo = seedIdle()
|
|
229
|
+
const { stdout, stderr, exitCode } = await run(repo, "install")
|
|
230
|
+
expect(exitCode).toBe(0)
|
|
231
|
+
expect(stdout).toMatch(/Make sure a .gtd\.config\.ts. governs the repository/)
|
|
232
|
+
expect(stderr).not.toContain(WORKFLOWS_REPO)
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
describe("gtd --workflow <name> — a workflow that fixes its diff base", () => {
|
|
237
|
+
const WORKFLOW = `import { human } from "@pmelab/gtd/flows"
|
|
238
|
+
|
|
239
|
+
export default async () => {
|
|
240
|
+
await human("idle", { message: "hi" })
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export const review = async () => {
|
|
244
|
+
await human("checking", { message: "check" })
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export const defaults = { reviewBase: "" }
|
|
248
|
+
|
|
249
|
+
export const base = (workflow, vars) => (workflow === "review" ? vars.reviewBase : undefined)
|
|
250
|
+
`
|
|
251
|
+
|
|
198
252
|
it("--workflow review --var reviewBase=<base> emits a script anchoring the process to that base", async () => {
|
|
199
|
-
const repo =
|
|
253
|
+
const repo = seed(WORKFLOW)
|
|
200
254
|
const base = repo.commitHistory().at(-1)!.hash
|
|
201
255
|
repo.writeFile("scratch.txt", "more work\n")
|
|
202
256
|
repo.seedCommit("chore: more work")
|
|
@@ -209,26 +263,16 @@ describe("gtd --workflow <name> — the shipped workflows", () => {
|
|
|
209
263
|
`reviewBase=${base}`,
|
|
210
264
|
)
|
|
211
265
|
expect(exitCode).toBe(0)
|
|
212
|
-
expect(stdout).toContain("gtd(human):
|
|
266
|
+
expect(stdout).toContain("gtd(human): checking")
|
|
213
267
|
expect(stdout).toContain("Gtd-Workflow: review")
|
|
214
268
|
expect(stdout).toContain(`Gtd-Review-Base: ${base}`)
|
|
215
269
|
expect(repo.commitHistory()).toHaveLength(before)
|
|
216
270
|
})
|
|
217
|
-
|
|
218
|
-
it("--workflow fix emits a script that would start a fix process", async () => {
|
|
219
|
-
const repo = seedBundled()
|
|
220
|
-
const before = repo.commitHistory().length
|
|
221
|
-
const { stdout, exitCode } = await run(repo, "--workflow", "fix")
|
|
222
|
-
expect(exitCode).toBe(0)
|
|
223
|
-
expect(stdout).toContain("gtd(human): fix-precheck")
|
|
224
|
-
expect(stdout).toContain("Gtd-Workflow: fix")
|
|
225
|
-
expect(repo.commitHistory()).toHaveLength(before)
|
|
226
|
-
})
|
|
227
271
|
})
|
|
228
272
|
|
|
229
273
|
describe("config loading", () => {
|
|
230
274
|
it('gtd next --verbose narrates "config: module" exactly once — the extra load that surfaces warnings is silent', async () => {
|
|
231
|
-
const repo =
|
|
275
|
+
const repo = seedIdle()
|
|
232
276
|
const { stderr, exitCode } = await run(repo, "next", "--verbose")
|
|
233
277
|
expect(exitCode).toBe(0)
|
|
234
278
|
expect(stderr.match(/config: module /g)).toHaveLength(1)
|
|
@@ -287,7 +331,7 @@ describe("gtd next --json — log path emission", () => {
|
|
|
287
331
|
const seededRepo = (): InMemRepo => {
|
|
288
332
|
const repo = new InMemRepo()
|
|
289
333
|
repo.writeFile("NOTE.md", "a note\n")
|
|
290
|
-
repo.writeFile("gtd.config.ts",
|
|
334
|
+
repo.writeFile("gtd.config.ts", IDLE_THEN_WORKING)
|
|
291
335
|
repo.seedCommit("gtd: init")
|
|
292
336
|
return repo
|
|
293
337
|
}
|
|
@@ -1702,7 +1746,7 @@ describe("runCommand — refuses in a repository with no commits", () => {
|
|
|
1702
1746
|
)
|
|
1703
1747
|
|
|
1704
1748
|
const NO_COMMITS_MESSAGE =
|
|
1705
|
-
"gtd
|
|
1749
|
+
"gtd: the repository has no commits yet — make an initial commit, then run gtd again"
|
|
1706
1750
|
|
|
1707
1751
|
it("derives exactly the eighteen non-standalone kinds — a canary for the table-driven cases below", () => {
|
|
1708
1752
|
expect(stateKinds.sort()).toEqual(
|
|
@@ -1752,7 +1796,7 @@ describe("runCommand — refuses in a repository with no commits", () => {
|
|
|
1752
1796
|
)
|
|
1753
1797
|
|
|
1754
1798
|
it("a repository with a commit passes the guard and dispatches normally", async () => {
|
|
1755
|
-
const repo =
|
|
1799
|
+
const repo = seedIdle()
|
|
1756
1800
|
const written: string[] = []
|
|
1757
1801
|
const out = { write: (chunk: string) => written.push(chunk), flush: () => {} }
|
|
1758
1802
|
|
|
@@ -1765,7 +1809,7 @@ describe("runCommand — refuses in a repository with no commits", () => {
|
|
|
1765
1809
|
})
|
|
1766
1810
|
|
|
1767
1811
|
it("gtd ui in a repository WITH commits reaches its own dispatch, past the guard", async () => {
|
|
1768
|
-
const repo =
|
|
1812
|
+
const repo = seedIdle()
|
|
1769
1813
|
const out = { write: () => {}, flush: () => {} }
|
|
1770
1814
|
const requests: unknown[] = []
|
|
1771
1815
|
const ui = (request: unknown) => Effect.sync(() => void requests.push(request))
|
|
@@ -1950,7 +1994,7 @@ describe("gtd next/land --json=<path> — the select branch", () => {
|
|
|
1950
1994
|
const seededRepo = (): InMemRepo => {
|
|
1951
1995
|
const repo = new InMemRepo()
|
|
1952
1996
|
repo.writeFile("NOTE.md", "a note\n")
|
|
1953
|
-
repo.writeFile("gtd.config.ts",
|
|
1997
|
+
repo.writeFile("gtd.config.ts", IDLE_THEN_WORKING)
|
|
1954
1998
|
repo.seedCommit("gtd: init")
|
|
1955
1999
|
return repo
|
|
1956
2000
|
}
|
|
@@ -1987,7 +2031,7 @@ describe("gtd next/land --json=<path> — the select branch", () => {
|
|
|
1987
2031
|
|
|
1988
2032
|
it("an absent selector writes zero bytes to stdout and exits EXIT_OK", async () => {
|
|
1989
2033
|
const repo = seededRepo()
|
|
1990
|
-
// The
|
|
2034
|
+
// The workflow's initial `idle` step declares no `mode:` —
|
|
1991
2035
|
// `mode` is a real, present-but-`undefined` field on the beat document
|
|
1992
2036
|
// there (see `src/wire/BeatStatus.ts`'s `statusOf`), not merely an
|
|
1993
2037
|
// unknown path.
|
|
@@ -2263,3 +2307,53 @@ export const defaults = ${vars}
|
|
|
2263
2307
|
expect(repo.lastCommitMessage()).not.toContain("Gtd-Payload:")
|
|
2264
2308
|
})
|
|
2265
2309
|
})
|
|
2310
|
+
|
|
2311
|
+
describe("runCommand — the repository-root guard's messages", () => {
|
|
2312
|
+
const guarded = async (
|
|
2313
|
+
override: (
|
|
2314
|
+
git: Context.Tag.Service<GitService>,
|
|
2315
|
+
host: Context.Tag.Service<Host>,
|
|
2316
|
+
) => Effect.Effect<unknown, Error, CommandRequirements>,
|
|
2317
|
+
): Promise<string> => {
|
|
2318
|
+
const exit = await Effect.runPromiseExit(
|
|
2319
|
+
Effect.gen(function* () {
|
|
2320
|
+
return yield* override(yield* GitService, yield* Host)
|
|
2321
|
+
}).pipe(Effect.provide(testLayers(seedIdle()))),
|
|
2322
|
+
)
|
|
2323
|
+
return Exit.isFailure(exit) ? (Cause.squash(exit.cause) as Error).message : ""
|
|
2324
|
+
}
|
|
2325
|
+
const out = { write: () => {}, flush: () => {} }
|
|
2326
|
+
const next = runCommand({ kind: "next" }, { kind: "off" }, out)
|
|
2327
|
+
|
|
2328
|
+
it("outside a git repository says so instead of quoting git", async () => {
|
|
2329
|
+
const message = await guarded((git) =>
|
|
2330
|
+
next.pipe(
|
|
2331
|
+
Effect.provideService(GitService, {
|
|
2332
|
+
...git,
|
|
2333
|
+
topLevel: () =>
|
|
2334
|
+
Effect.fail(
|
|
2335
|
+
new Error(
|
|
2336
|
+
"git rev-parse --show-toplevel failed (exit 128): fatal: not a git repository (or any of the parent directories): .git",
|
|
2337
|
+
),
|
|
2338
|
+
),
|
|
2339
|
+
}),
|
|
2340
|
+
),
|
|
2341
|
+
)
|
|
2342
|
+
expect(message).toBe("gtd: not a git repository — run gtd inside one (`git init` makes one)")
|
|
2343
|
+
})
|
|
2344
|
+
|
|
2345
|
+
it("from a subdirectory names the root to cd to", async () => {
|
|
2346
|
+
const message = await guarded((_git, host) =>
|
|
2347
|
+
next.pipe(
|
|
2348
|
+
Effect.provideService(Host, {
|
|
2349
|
+
...host,
|
|
2350
|
+
root: "/repo/my sub",
|
|
2351
|
+
realPath: (path: string) => Effect.succeed(path),
|
|
2352
|
+
}),
|
|
2353
|
+
),
|
|
2354
|
+
)
|
|
2355
|
+
expect(message).toBe(
|
|
2356
|
+
"gtd: run gtd from the repository root — cd /repo (the current directory is /repo/my sub)",
|
|
2357
|
+
)
|
|
2358
|
+
})
|
|
2359
|
+
})
|
package/src/cli/Program.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
landFields,
|
|
34
34
|
noopText,
|
|
35
35
|
renderBeatJson,
|
|
36
|
+
DEMAND_BRIEFING,
|
|
36
37
|
renderBeatPlain,
|
|
37
38
|
renderLandJson,
|
|
38
39
|
renderLandPlain,
|
|
@@ -87,7 +88,7 @@ import {
|
|
|
87
88
|
abandonNoopOutcome,
|
|
88
89
|
restoredOutcome,
|
|
89
90
|
} from "../emit/index.js"
|
|
90
|
-
import {
|
|
91
|
+
import { renderBriefing, WORKFLOWS_REPO } from "../install/index.js"
|
|
91
92
|
import { ToolCommands, type SelfGtd } from "./ToolCommands.js"
|
|
92
93
|
import {
|
|
93
94
|
SelectorUsageError,
|
|
@@ -143,7 +144,7 @@ export type Command =
|
|
|
143
144
|
readonly vars: Readonly<Record<string, string>>
|
|
144
145
|
readonly label: string
|
|
145
146
|
}
|
|
146
|
-
| { readonly kind: "door"; readonly name
|
|
147
|
+
| { readonly kind: "door"; readonly name?: string; readonly args: readonly string[] }
|
|
147
148
|
| { readonly kind: "doors" }
|
|
148
149
|
| { readonly kind: "abandon" }
|
|
149
150
|
| { readonly kind: "restore" }
|
|
@@ -207,13 +208,12 @@ const runLspCommand = (): Effect.Effect<void, Error, ToolCommands> =>
|
|
|
207
208
|
|
|
208
209
|
/**
|
|
209
210
|
* Prints the briefing, emitted by the binary itself so it's always current.
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* Anywhere else it writes nothing.
|
|
211
|
+
* gtd ships no workflow, so where no `gtd.config.ts` governs it says where to
|
|
212
|
+
* get one; it writes nothing.
|
|
213
213
|
*/
|
|
214
214
|
const runInstallCommand = (
|
|
215
215
|
out: ArtifactOut,
|
|
216
|
-
): Effect.Effect<void, Error, GitService |
|
|
216
|
+
): Effect.Effect<void, Error, GitService | Host | ConfigDiscovery | Narrator> =>
|
|
217
217
|
Effect.gen(function* () {
|
|
218
218
|
const briefing = renderBriefing()
|
|
219
219
|
out.write(briefing.endsWith("\n") ? briefing : briefing + "\n")
|
|
@@ -222,9 +222,8 @@ const runInstallCommand = (
|
|
|
222
222
|
if (topLevel._tag === "Left") return
|
|
223
223
|
if ((yield* host.realPath(topLevel.right)) !== (yield* host.realPath(host.root))) return
|
|
224
224
|
if ((yield* (yield* ConfigDiscovery).workflowModule(host.root, host.home)) !== undefined) return
|
|
225
|
-
yield* (yield* Workspace).write(WORKFLOW_MODULE, CONFIG_SCAFFOLD)
|
|
226
225
|
yield* (yield* Narrator).warn(
|
|
227
|
-
`gtd install:
|
|
226
|
+
`gtd install: no ${WORKFLOW_MODULE} governs here, and gtd ships no workflow — get one from ${WORKFLOWS_REPO} or write your own (step 2 of the briefing above)`,
|
|
228
227
|
)
|
|
229
228
|
})
|
|
230
229
|
|
|
@@ -568,10 +567,12 @@ const runStartCommand = (
|
|
|
568
567
|
const config = yield* (yield* ConfigService).load
|
|
569
568
|
const resolved = config.workflowNamed(workflow)
|
|
570
569
|
if (resolved === undefined) {
|
|
570
|
+
const startable =
|
|
571
|
+
config.workflowNames.length > 0
|
|
572
|
+
? `startable: ${config.workflowNames.join(", ")}`
|
|
573
|
+
: "this config exports no workflow besides its default, which an ordinary start runs"
|
|
571
574
|
return yield* Effect.fail(
|
|
572
|
-
new GtdUsageError(
|
|
573
|
-
`gtd --workflow: unknown workflow "${workflow}" — startable: ${config.workflowNames.join(", ")}`,
|
|
574
|
-
),
|
|
575
|
+
new GtdUsageError(`gtd --workflow: unknown workflow "${workflow}" — ${startable}`),
|
|
575
576
|
)
|
|
576
577
|
}
|
|
577
578
|
const rest = yield* restAt(undefined, varOverrides)
|
|
@@ -593,35 +594,57 @@ const runStartCommand = (
|
|
|
593
594
|
if (plan.kind === "refusal") {
|
|
594
595
|
return yield* Effect.fail(new Error(plan.message))
|
|
595
596
|
}
|
|
596
|
-
out.write(landingScript(ScriptSurface.render(plan.steps)))
|
|
597
|
+
out.write(listingCommitted(landingScript(ScriptSurface.render(plan.steps)), rest.changes))
|
|
597
598
|
})
|
|
598
599
|
|
|
600
|
+
/**
|
|
601
|
+
* A start commits the whole tree (`git add -A`), so its script says up front
|
|
602
|
+
* which paths that is. Comment lines only: control characters are masked so a
|
|
603
|
+
* path can never end the comment and become a command.
|
|
604
|
+
*/
|
|
605
|
+
const listingCommitted = (script: string, changes: readonly PendingChange[]): string => {
|
|
606
|
+
if (changes.length === 0) return script
|
|
607
|
+
const listing = [
|
|
608
|
+
"# commits every change in the working tree (git add -A):",
|
|
609
|
+
...changes.map((change) => `# ${change.status} ${change.path.replace(/\p{Cc}/gu, "?")}`),
|
|
610
|
+
].join("\n")
|
|
611
|
+
const at = script.indexOf("\n\n")
|
|
612
|
+
return at === -1 ? script : `${script.slice(0, at)}\n\n${listing}${script.slice(at)}`
|
|
613
|
+
}
|
|
614
|
+
|
|
599
615
|
const doorSynopsis = (name: string, door: Door): string =>
|
|
600
616
|
[
|
|
601
617
|
name,
|
|
602
618
|
...(door.args ?? []).map((arg) => (arg.optional === true ? `[${arg.name}]` : `<${arg.name}>`)),
|
|
603
619
|
].join(" ")
|
|
604
620
|
|
|
621
|
+
const noSuchDoor = (name: string | undefined, doors: ReadonlyMap<string, Door>): GtdUsageError => {
|
|
622
|
+
const listing =
|
|
623
|
+
doors.size > 0
|
|
624
|
+
? `doors: ${[...doors].map(([n, d]) => doorSynopsis(n, d)).join(", ")}`
|
|
625
|
+
: "this config has no doors"
|
|
626
|
+
return new GtdUsageError(
|
|
627
|
+
name === undefined
|
|
628
|
+
? `gtd door: missing door name — ${listing}`
|
|
629
|
+
: `gtd door: unknown door "${name}" — ${listing}`,
|
|
630
|
+
)
|
|
631
|
+
}
|
|
632
|
+
|
|
605
633
|
/**
|
|
606
634
|
* `gtd door <name> [args…]`: `gtd --workflow` with the door's workflow and the
|
|
607
635
|
* process settings its `vars` derive from the positional args. A wrong name or
|
|
608
636
|
* arity is a usage error naming what would have worked.
|
|
609
637
|
*/
|
|
610
638
|
const runDoorCommand = (
|
|
611
|
-
name: string,
|
|
639
|
+
name: string | undefined,
|
|
612
640
|
args: readonly string[],
|
|
613
641
|
out: ArtifactOut,
|
|
614
642
|
): Effect.Effect<void, Error, CommandRequirements> =>
|
|
615
643
|
Effect.gen(function* () {
|
|
616
644
|
const config = yield* (yield* ConfigService).load
|
|
617
|
-
const door = config.doors.get(name)
|
|
618
|
-
if (door === undefined) {
|
|
619
|
-
|
|
620
|
-
return yield* Effect.fail(
|
|
621
|
-
new GtdUsageError(
|
|
622
|
-
`gtd door: unknown door "${name}" — doors: ${known.map(([n, d]) => doorSynopsis(n, d)).join(", ")}`,
|
|
623
|
-
),
|
|
624
|
-
)
|
|
645
|
+
const door = name === undefined ? undefined : config.doors.get(name)
|
|
646
|
+
if (name === undefined || door === undefined) {
|
|
647
|
+
return yield* Effect.fail(noSuchDoor(name, config.doors))
|
|
625
648
|
}
|
|
626
649
|
const declared = door.args ?? []
|
|
627
650
|
const required = declared.filter((arg) => arg.optional !== true).length
|
|
@@ -652,6 +675,11 @@ const runDoorsCommand = (
|
|
|
652
675
|
const config = yield* (yield* ConfigService).load
|
|
653
676
|
const doors = [...config.doors]
|
|
654
677
|
if (json.kind === "off") {
|
|
678
|
+
if (doors.length === 0) {
|
|
679
|
+
yield* (yield* Narrator).warn(
|
|
680
|
+
"gtd doors: this config has no doors — a workflow factory's `doors` option or the config's `doors` export adds one",
|
|
681
|
+
)
|
|
682
|
+
}
|
|
655
683
|
out.write(doors.map(([n, d]) => `${doorSynopsis(n, d)} → ${d.workflow}\n`).join(""))
|
|
656
684
|
return
|
|
657
685
|
}
|
|
@@ -851,6 +879,12 @@ const runNextCommand = (
|
|
|
851
879
|
const validateCommand = emitsValidatablePrompt(rendered)
|
|
852
880
|
? selfValidateCommand(rest.def, rest.state, rendered.mode!, rendered.file!)
|
|
853
881
|
: undefined
|
|
882
|
+
if (DEMAND_BRIEFING[fields.kind].suppressHeader) {
|
|
883
|
+
// A prompt's stdout is the agent's input, so its header goes to stderr;
|
|
884
|
+
// rendered as a header-showing kind to reuse the one header layout.
|
|
885
|
+
const shown = renderBeatPlain({ ...fields, kind: "message" })
|
|
886
|
+
yield* narrator.warn(shown.slice(0, shown.indexOf("\n\n")))
|
|
887
|
+
}
|
|
854
888
|
out.write(renderBeatPlain(fields, validateCommand))
|
|
855
889
|
}
|
|
856
890
|
})
|
|
@@ -1127,19 +1161,30 @@ const runUiCliCommand = (
|
|
|
1127
1161
|
*/
|
|
1128
1162
|
const assertRunningFromRepoRoot = (git: GitOperations, host: HostOps): Effect.Effect<void, Error> =>
|
|
1129
1163
|
Effect.gen(function* () {
|
|
1130
|
-
const topLevel = yield* git
|
|
1164
|
+
const topLevel = yield* git
|
|
1165
|
+
.topLevel()
|
|
1166
|
+
.pipe(
|
|
1167
|
+
Effect.mapError((e) =>
|
|
1168
|
+
/not a git repository/.test(e.message)
|
|
1169
|
+
? new Error("gtd: not a git repository — run gtd inside one (`git init` makes one)")
|
|
1170
|
+
: e,
|
|
1171
|
+
),
|
|
1172
|
+
)
|
|
1131
1173
|
const topReal = yield* host.realPath(topLevel)
|
|
1132
1174
|
const cwdReal = yield* host.realPath(host.root)
|
|
1133
1175
|
if (topReal !== cwdReal) {
|
|
1134
1176
|
return yield* Effect.fail(
|
|
1135
1177
|
new Error(
|
|
1136
|
-
`gtd
|
|
1137
|
-
`the current directory is ${host.root}`,
|
|
1178
|
+
`gtd: run gtd from the repository root — cd ${shellWord(topLevel)} ` +
|
|
1179
|
+
`(the current directory is ${host.root})`,
|
|
1138
1180
|
),
|
|
1139
1181
|
)
|
|
1140
1182
|
}
|
|
1141
1183
|
})
|
|
1142
1184
|
|
|
1185
|
+
const shellWord = (path: string): string =>
|
|
1186
|
+
/^[\w@%+=:,./-]+$/.test(path) ? path : `'${path.replace(/'/g, "'\\''")}'`
|
|
1187
|
+
|
|
1143
1188
|
/** A command that derives workflow state needs at least one commit too — a commitless repository has no HEAD to resolve a `Rest` at. */
|
|
1144
1189
|
const assertRepositoryHasCommits = (git: GitOperations): Effect.Effect<void, Error> =>
|
|
1145
1190
|
Effect.gen(function* () {
|
|
@@ -1147,7 +1192,7 @@ const assertRepositoryHasCommits = (git: GitOperations): Effect.Effect<void, Err
|
|
|
1147
1192
|
if (!hasCommits) {
|
|
1148
1193
|
return yield* Effect.fail(
|
|
1149
1194
|
new Error(
|
|
1150
|
-
"gtd
|
|
1195
|
+
"gtd: the repository has no commits yet — make an initial commit, then run gtd again",
|
|
1151
1196
|
),
|
|
1152
1197
|
)
|
|
1153
1198
|
}
|
package/src/edge/Edge.ts
CHANGED
|
@@ -995,7 +995,7 @@ export const restAt = (
|
|
|
995
995
|
if (stepDef.mode !== undefined && def.modes[stepDef.mode] === undefined) {
|
|
996
996
|
return yield* Effect.fail(
|
|
997
997
|
new Error(
|
|
998
|
-
`gtd config: step "${step.name}": mode "${stepDef.mode}" is not a mode this workflow knows (${knownModes(def).join(", ")})`,
|
|
998
|
+
`gtd: config: step "${step.name}": mode "${stepDef.mode}" is not a mode this workflow knows (${knownModes(def).join(", ")})`,
|
|
999
999
|
),
|
|
1000
1000
|
)
|
|
1001
1001
|
}
|
|
@@ -1476,12 +1476,12 @@ const historySteps = (run: ProcessRun): readonly HistoryStep[] => {
|
|
|
1476
1476
|
const versionWarningFor = (recorded: string | undefined): string | undefined =>
|
|
1477
1477
|
recorded === GTD_VERSION
|
|
1478
1478
|
? undefined
|
|
1479
|
-
: `gtd: warning: this history was recorded by ${recorded === undefined ? "an unknown gtd version" : `gtd ${recorded}`} and is replayed by gtd ${GTD_VERSION} —
|
|
1479
|
+
: `gtd: warning: this history was recorded by ${recorded === undefined ? "an unknown gtd version" : `gtd ${recorded}`} and is replayed by gtd ${GTD_VERSION} — the engine the workflow runs on may have changed since`
|
|
1480
1480
|
|
|
1481
1481
|
/**
|
|
1482
1482
|
* Replays the closed run at `tip` under today's config. The workflow is not
|
|
1483
|
-
* fingerprinted for a version change:
|
|
1484
|
-
*
|
|
1483
|
+
* fingerprinted for a version change: the engine under it moves with every
|
|
1484
|
+
* upgrade, and the version warning is the only signal.
|
|
1485
1485
|
*/
|
|
1486
1486
|
export const historyTrail = (tip: string): Effect.Effect<HistoryTrail, Error, RestRequirements> =>
|
|
1487
1487
|
Effect.gen(function* () {
|
package/src/emit/Emit.test.ts
CHANGED
|
@@ -359,7 +359,7 @@ describe("real repo — an assembled script end to end", () => {
|
|
|
359
359
|
describe("combinedScript — the plain-text write commands' single pasteable script", () => {
|
|
360
360
|
it("DID_NOT_RUN_COMMENT points the reader at sh, not bash", () => {
|
|
361
361
|
expect(DID_NOT_RUN_COMMENT).toBe(
|
|
362
|
-
"# gtd emitted this and did NOT run it —
|
|
362
|
+
"# gtd emitted this and did NOT run it — run it with `sh` to apply it",
|
|
363
363
|
)
|
|
364
364
|
expect(DID_NOT_RUN_COMMENT).not.toContain("bash")
|
|
365
365
|
})
|
package/src/emit/Emit.ts
CHANGED
|
@@ -112,7 +112,7 @@ export const emitScripts = (
|
|
|
112
112
|
|
|
113
113
|
/** Exported so `src/twins/EmittedScriptRecognizer.ts` can recognize these by exact string comparison. `printf`, not `echo`, avoids shell-specific backslash-escape quirks. */
|
|
114
114
|
export const DID_NOT_RUN_COMMENT =
|
|
115
|
-
"# gtd emitted this and did NOT run it —
|
|
115
|
+
"# gtd emitted this and did NOT run it — run it with `sh` to apply it"
|
|
116
116
|
|
|
117
117
|
export const PRESENTATION_ONLY_COMMENT = "# presentation only — safe to skip"
|
|
118
118
|
|
package/src/flags/index.test.ts
CHANGED
|
@@ -102,16 +102,3 @@ describe("flagSettings", () => {
|
|
|
102
102
|
})
|
|
103
103
|
})
|
|
104
104
|
})
|
|
105
|
-
|
|
106
|
-
describe("risk_tier", () => {
|
|
107
|
-
it("is a release flag, off by default, resolved from GTD_FLAG_RISK_TIER", () => {
|
|
108
|
-
expect(FLAGS.find((f) => f.name === "risk_tier")).toMatchObject({ kind: "release" })
|
|
109
|
-
expect(resolveFlags({}).risk_tier).toBe(false)
|
|
110
|
-
expect(resolveFlags({ GTD_FLAG_RISK_TIER: "1" }).risk_tier).toBe(true)
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it("is pinned as flag_risk_tier", () => {
|
|
114
|
-
expect(flagSettings(resolveFlags({ GTD_FLAG_RISK_TIER: "1" })).flag_risk_tier).toBe("1")
|
|
115
|
-
expect(flagSettings(resolveFlags({})).flag_risk_tier).toBe("0")
|
|
116
|
-
})
|
|
117
|
-
})
|
package/src/flags/index.ts
CHANGED
|
@@ -13,9 +13,7 @@ export interface FlagDeclaration {
|
|
|
13
13
|
readonly defaultOn?: boolean
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export const FLAGS = [
|
|
17
|
-
{ name: "risk_tier", owner: "pmelab", expiry: "2027-01-15", kind: "release" },
|
|
18
|
-
] as const satisfies readonly FlagDeclaration[]
|
|
16
|
+
export const FLAGS = [] as const satisfies readonly FlagDeclaration[]
|
|
19
17
|
|
|
20
18
|
/** A misspelt name is a compile error. */
|
|
21
19
|
export type Flags<R extends readonly FlagDeclaration[] = typeof FLAGS> = {
|