@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
|
@@ -16,13 +16,14 @@ import {
|
|
|
16
16
|
agent,
|
|
17
17
|
check,
|
|
18
18
|
human,
|
|
19
|
+
judge,
|
|
19
20
|
moveScript,
|
|
20
21
|
read,
|
|
21
22
|
removeScript,
|
|
22
23
|
run,
|
|
23
24
|
showScript,
|
|
24
25
|
} from "../flows/index.js"
|
|
25
|
-
import { runsItsScript, testRepo } from "./TestRepo.js"
|
|
26
|
+
import { runsItsScript, testRepo, type Actor } from "./TestRepo.js"
|
|
26
27
|
|
|
27
28
|
const tiny = async (): Promise<void> => {
|
|
28
29
|
await agent("plan", "Write PLAN.md.")
|
|
@@ -201,7 +202,51 @@ describe("testRepo", () => {
|
|
|
201
202
|
let n = 0
|
|
202
203
|
await expect(
|
|
203
204
|
repo.drive({ spin: (_, r) => r.write("n", `${++n}\n`) }, { maxBeats: 5 }),
|
|
204
|
-
).rejects.toThrow(
|
|
205
|
+
).rejects.toThrow(
|
|
206
|
+
"did not rest within 5 beats; it passed through:\nspin → spin → spin → spin → spin",
|
|
207
|
+
)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
describe("judge verdicts", () => {
|
|
211
|
+
const judged = async (): Promise<void> => {
|
|
212
|
+
await agent("plan", "Plan.")
|
|
213
|
+
const { answers } = await judge("ready", {
|
|
214
|
+
questions: [{ id: "ready", primitive: "noul", instructions: "Ready?", criteria: "" }],
|
|
215
|
+
evidence: { plan: "p" },
|
|
216
|
+
})
|
|
217
|
+
await human(answers.ready?.answer === "yes" ? "ship" : "rework")
|
|
218
|
+
}
|
|
219
|
+
const plan: Actor = (_, r) => r.write("PLAN.md", "p\n")
|
|
220
|
+
const template =
|
|
221
|
+
'expected a JSON array, one entry per question — answer a boolean for a noul, a string for a choice, a number for a score; p between 0 and 1:\n[{ "id": "ready", "answer": true, "p": 0.9 }]'
|
|
222
|
+
|
|
223
|
+
it("echoes a refused verdict and the shape it should have", async () => {
|
|
224
|
+
const repo = testRepo({ workflow: { default: judged } })
|
|
225
|
+
repo.write("go", "\n")
|
|
226
|
+
const error = await repo
|
|
227
|
+
.drive({ plan, ready: () => '{"ready": true}' })
|
|
228
|
+
.catch((e: Error) => e)
|
|
229
|
+
expect((error as Error).message).toContain(
|
|
230
|
+
`the actor for judge step "ready" sent a verdict gtd refused:\n{"ready": true}\n${template}\ngtd judge answer exited 2\n`,
|
|
231
|
+
)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it("says an actor returned an object rather than no verdict", async () => {
|
|
235
|
+
const repo = testRepo({ workflow: { default: judged } })
|
|
236
|
+
repo.write("go", "\n")
|
|
237
|
+
const actor = (() => [{ id: "ready", answer: true, p: 0.9 }]) as unknown as Actor
|
|
238
|
+
await expect(repo.drive({ plan, ready: actor })).rejects.toThrow(
|
|
239
|
+
`the actor for judge step "ready" returned an array, not a JSON string — JSON.stringify it\n${template}`,
|
|
240
|
+
)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it("says an actor returned nothing", async () => {
|
|
244
|
+
const repo = testRepo({ workflow: { default: judged } })
|
|
245
|
+
repo.write("go", "\n")
|
|
246
|
+
await expect(repo.drive({ plan, ready: () => undefined })).rejects.toThrow(
|
|
247
|
+
`the actor for judge step "ready" returned no verdict\n${template}`,
|
|
248
|
+
)
|
|
249
|
+
})
|
|
205
250
|
})
|
|
206
251
|
|
|
207
252
|
describe("canned commands", () => {
|
package/src/testkit/TestRepo.ts
CHANGED
|
@@ -204,6 +204,30 @@ const runLines = (repo: InMemRepo, commands: CommandTable, script: string): void
|
|
|
204
204
|
const failed = (what: string, result: CapturedCliResult): Error =>
|
|
205
205
|
new Error(`${what} exited ${result.exitCode}\n${result.stderr}${result.stdout}`)
|
|
206
206
|
|
|
207
|
+
const ANSWER: Readonly<Record<string, string>> = {
|
|
208
|
+
noul: "true",
|
|
209
|
+
choice: '"<option>"',
|
|
210
|
+
score: "0.5",
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The verdict a judge beat's questions take, as a template to copy. */
|
|
214
|
+
const expectedVerdict = (doc: BeatDocument): string => {
|
|
215
|
+
let questions: readonly { id: string; primitive: string }[] = []
|
|
216
|
+
try {
|
|
217
|
+
questions = (JSON.parse(doc.judge ?? "") as { questions: typeof questions }).questions
|
|
218
|
+
} catch {
|
|
219
|
+
// An unreadable judge document leaves the template without entries.
|
|
220
|
+
}
|
|
221
|
+
const entries = questions.map(
|
|
222
|
+
({ id, primitive }) =>
|
|
223
|
+
`{ "id": ${JSON.stringify(id)}, "answer": ${ANSWER[primitive] ?? '"<answer>"'}, "p": 0.9 }`,
|
|
224
|
+
)
|
|
225
|
+
return [
|
|
226
|
+
"expected a JSON array, one entry per question — answer a boolean for a noul, a string for a choice, a number for a score; p between 0 and 1:",
|
|
227
|
+
`[${entries.join(", ")}]`,
|
|
228
|
+
].join("\n")
|
|
229
|
+
}
|
|
230
|
+
|
|
207
231
|
export const testRepo = (options: TestRepoOptions): TestRepo => {
|
|
208
232
|
const repo = new InMemRepo()
|
|
209
233
|
const env = { ...options.env }
|
|
@@ -261,11 +285,15 @@ export const testRepo = (options: TestRepoOptions): TestRepo => {
|
|
|
261
285
|
}
|
|
262
286
|
|
|
263
287
|
/** Runs a write command, then the script its `--json=script` twin prints. */
|
|
264
|
-
const runWrite = async (
|
|
288
|
+
const runWrite = async (
|
|
289
|
+
args: readonly string[],
|
|
290
|
+
stdin?: string,
|
|
291
|
+
refused: (error: Error) => Error = (error) => error,
|
|
292
|
+
): Promise<CapturedCliResult> => {
|
|
265
293
|
const call = (...extra: string[]) =>
|
|
266
294
|
stdin === undefined ? gtd(...args, ...extra) : withStdin(stdin, () => gtd(...args, ...extra))
|
|
267
295
|
const reported = await call()
|
|
268
|
-
if (reported.exitCode !== EXIT_OK) throw failed(`gtd ${args.join(" ")}`, reported)
|
|
296
|
+
if (reported.exitCode !== EXIT_OK) throw refused(failed(`gtd ${args.join(" ")}`, reported))
|
|
269
297
|
const script = await call("--json=script")
|
|
270
298
|
apply(`gtd ${args.join(" ")}`, script.stdout)
|
|
271
299
|
return reported
|
|
@@ -286,16 +314,29 @@ export const testRepo = (options: TestRepoOptions): TestRepo => {
|
|
|
286
314
|
}
|
|
287
315
|
|
|
288
316
|
/** Lands what an actor did: a judge's verdict answered, anything else validated and landed. */
|
|
289
|
-
const act = async (doc: BeatDocument, verdict:
|
|
317
|
+
const act = async (doc: BeatDocument, verdict: unknown): Promise<void> => {
|
|
290
318
|
if (doc.actor !== "judge") {
|
|
291
319
|
if (doc.validate !== undefined) apply(`validating ${doc.file ?? doc.state}`, doc.validate)
|
|
292
320
|
await runWrite(["land"])
|
|
293
321
|
return
|
|
294
322
|
}
|
|
323
|
+
const actor = `the actor for judge step "${doc.state}"`
|
|
324
|
+
if (verdict === undefined) {
|
|
325
|
+
throw new Error(`${actor} returned no verdict\n${expectedVerdict(doc)}`)
|
|
326
|
+
}
|
|
295
327
|
if (typeof verdict !== "string") {
|
|
296
|
-
throw new Error(
|
|
328
|
+
throw new Error(
|
|
329
|
+
`${actor} returned ${Array.isArray(verdict) ? "an array" : typeof verdict === "object" ? "an object" : `a ${typeof verdict}`}, not a JSON string — JSON.stringify it\n${expectedVerdict(doc)}`,
|
|
330
|
+
)
|
|
297
331
|
}
|
|
298
|
-
await runWrite(
|
|
332
|
+
await runWrite(
|
|
333
|
+
["judge", "answer"],
|
|
334
|
+
verdict,
|
|
335
|
+
(error) =>
|
|
336
|
+
new Error(
|
|
337
|
+
`${actor} sent a verdict gtd refused:\n${verdict}\n${expectedVerdict(doc)}\n${error.message}`,
|
|
338
|
+
),
|
|
339
|
+
)
|
|
299
340
|
}
|
|
300
341
|
|
|
301
342
|
const self: TestRepo = {
|
|
@@ -327,8 +368,10 @@ export const testRepo = (options: TestRepoOptions): TestRepo => {
|
|
|
327
368
|
land: () => runWrite(["land"]),
|
|
328
369
|
next,
|
|
329
370
|
drive: async (actors = {}, { maxBeats = 50 } = {}) => {
|
|
371
|
+
const passed: string[] = []
|
|
330
372
|
for (let beat = 0; beat < maxBeats; beat++) {
|
|
331
373
|
const doc = await next()
|
|
374
|
+
passed.push(doc.state)
|
|
332
375
|
if (doc.kind === "stalled") return doc
|
|
333
376
|
if (doc.kind === "capture") {
|
|
334
377
|
await runWrite(["land"])
|
|
@@ -338,7 +381,9 @@ export const testRepo = (options: TestRepoOptions): TestRepo => {
|
|
|
338
381
|
if (actor === undefined) return doc
|
|
339
382
|
await act(doc, await actor(doc, self))
|
|
340
383
|
}
|
|
341
|
-
throw new Error(
|
|
384
|
+
throw new Error(
|
|
385
|
+
`the process did not rest within ${maxBeats} beats; it passed through:\n${passed.join(" → ")}`,
|
|
386
|
+
)
|
|
342
387
|
},
|
|
343
388
|
subjects: () => repo.commitHistory().map(({ message }) => message.split("\n")[0] ?? ""),
|
|
344
389
|
messages: () => repo.commitHistory().map(({ message }) => message),
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
softResetTo,
|
|
17
17
|
updateRef,
|
|
18
18
|
binaryGuard,
|
|
19
|
-
DID_NOT_RUN_COMMENT,
|
|
20
19
|
failurePromptWrapper,
|
|
21
20
|
fileExistsGuard,
|
|
22
21
|
PRESENTATION_FAILURE_WARNING,
|
|
@@ -552,13 +551,11 @@ const recognizeGtdUncheck = (repo: InMemRepo, block: string): BlockOutcome | und
|
|
|
552
551
|
}
|
|
553
552
|
|
|
554
553
|
/**
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
* script, so recognizing it as a no-op (like the retry-helper function
|
|
558
|
-
* definition) keeps the `@inmem` tier green.
|
|
554
|
+
* A block of nothing but comments is a no-op in `sh` too — `combinedScript`'s
|
|
555
|
+
* leading `DID_NOT_RUN_COMMENT`, and the list of paths a start commits.
|
|
559
556
|
*/
|
|
560
|
-
const
|
|
561
|
-
block
|
|
557
|
+
const recognizeCommentBlock = (block: string): BlockOutcome | undefined =>
|
|
558
|
+
block.split("\n").every((line) => line.startsWith("#")) ? { kind: "noop" } : undefined
|
|
562
559
|
|
|
563
560
|
/** `combinedScript`'s optional half. Reports `noop` whatever the inner result, mirroring bash's `( … ) || <warning>`. Exception: under `git: false` any unrecognized inner block fails the outer script (git or not), so the in-memory twin knowingly diverges from `sh` rather than run git unseen. */
|
|
564
561
|
const PRESENTATION_FAILURE_TEXT = `${extractQuotedTokens(PRESENTATION_FAILURE_WARNING)[0]!.replace(/\\n$/, "")}\n`
|
|
@@ -695,7 +692,7 @@ const recognizersFor = (
|
|
|
695
692
|
git: boolean,
|
|
696
693
|
): ReadonlyArray<(block: string) => BlockOutcome | undefined> => [
|
|
697
694
|
(block) => recognizeSetFlags(block),
|
|
698
|
-
(block) =>
|
|
695
|
+
(block) => recognizeCommentBlock(block),
|
|
699
696
|
(block) => recognizePrecondition(repo, block),
|
|
700
697
|
(block) => recognizeFileExistsGuard(repo, block),
|
|
701
698
|
(block) => recognizeBinaryGuard(commands, block),
|
package/src/twins/FlowScripts.ts
CHANGED
|
@@ -224,7 +224,7 @@ const recognizePatch = (repo: InMemRepo, script: string): FlowScriptResult | und
|
|
|
224
224
|
|
|
225
225
|
type PreambleOutcome = { readonly exit: boolean; readonly output: string }
|
|
226
226
|
|
|
227
|
-
/** One preamble line — the closed vocabulary
|
|
227
|
+
/** One preamble line — the closed vocabulary workflows write there. */
|
|
228
228
|
const runPreambleLine = (repo: InMemRepo, line: string): PreambleOutcome => {
|
|
229
229
|
const quoted = words(line)
|
|
230
230
|
if (line === "exit 0") return { exit: true, output: "" }
|
package/src/wire/Land.ts
CHANGED
|
@@ -71,5 +71,5 @@ export const landProseText = (subject: string): string => renderFormat(FMT_LAND_
|
|
|
71
71
|
*/
|
|
72
72
|
export const renderLandPlain = (fields: LandFields, command = "land"): string =>
|
|
73
73
|
fields.subject !== null
|
|
74
|
-
? `${landProseText(fields.subject).trimEnd()}\n(
|
|
74
|
+
? `${landProseText(fields.subject).trimEnd()}\n(to land it: s=$(gtd ${command} --json=script) && sh -c "$s")\n`
|
|
75
75
|
: noopText(fields.state)
|
package/src/workflow/index.ts
CHANGED
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
resolveVars,
|
|
16
16
|
seededValidateCommand,
|
|
17
17
|
} from "./index.js"
|
|
18
|
-
import { CONFIG_SCAFFOLD } from "../install/index.js"
|
|
19
18
|
|
|
20
19
|
// Every real port `ConfigService.Live` needs, scoped to `dir` — `home` is a
|
|
21
20
|
// fresh directory above the project, so the walk stops there as it would in a
|
|
@@ -71,12 +70,109 @@ const loadAndResolve = (dir: string = projectDir, vars: Env = {}) =>
|
|
|
71
70
|
let homeDir: string
|
|
72
71
|
let projectDir: string
|
|
73
72
|
|
|
74
|
-
//
|
|
73
|
+
// A config whose default workflow rests at `idle`, declaring a process
|
|
74
|
+
// setting, an environment setting and the scopes its turns run under.
|
|
75
|
+
const BASE_CONFIG = `import { human } from "@pmelab/gtd/flows"
|
|
76
|
+
|
|
77
|
+
export default async () => {
|
|
78
|
+
await human("idle", { message: "hi" })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const defaults = { qualityReviews: "" }
|
|
82
|
+
export const envDefaults = { testCommand: "npm test" }
|
|
83
|
+
|
|
84
|
+
export const skills = (vars) => ({
|
|
85
|
+
build: [],
|
|
86
|
+
"build.fix.quality": [],
|
|
87
|
+
"build.review": [],
|
|
88
|
+
...Object.fromEntries(
|
|
89
|
+
(vars.qualityReviews ?? "")
|
|
90
|
+
.split(",")
|
|
91
|
+
.map((lens) => lens.trim())
|
|
92
|
+
.filter((lens) => lens !== "")
|
|
93
|
+
.map((lens) => [\`build.quality.\${lens}\`, []]),
|
|
94
|
+
),
|
|
95
|
+
})
|
|
96
|
+
`
|
|
97
|
+
|
|
98
|
+
// Workflow factories a config imports from a sibling module: string options
|
|
99
|
+
// with defaults, an unknown or non-string option being an issue.
|
|
100
|
+
const FACTORIES = `import { defineWorkflow, human } from "@pmelab/gtd/flows"
|
|
101
|
+
|
|
102
|
+
const settings = (known) => ({
|
|
103
|
+
"~standard": {
|
|
104
|
+
version: 1,
|
|
105
|
+
vendor: "test",
|
|
106
|
+
validate: (value) => {
|
|
107
|
+
const issues = Object.entries(value).flatMap(([key, given]) =>
|
|
108
|
+
!Object.hasOwn(known, key)
|
|
109
|
+
? [{ path: [key], message: \`is not an option — options: \${Object.keys(known).join(", ")}\` }]
|
|
110
|
+
: typeof given === "string"
|
|
111
|
+
? []
|
|
112
|
+
: [{ path: [key], message: "must be a string" }],
|
|
113
|
+
)
|
|
114
|
+
return issues.length > 0 ? { issues } : { value: { ...known, ...value } }
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
export const feature = defineWorkflow(
|
|
120
|
+
settings({ qualityReviews: "", reviewNoteActionable: "0.7", testCommand: "npm test" }),
|
|
121
|
+
({ testCommand, ...defaults }) => ({
|
|
122
|
+
flow: async () => {
|
|
123
|
+
await human("idle")
|
|
124
|
+
},
|
|
125
|
+
defaults,
|
|
126
|
+
envDefaults: { testCommand },
|
|
127
|
+
}),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
export const review = defineWorkflow(settings({ reviewNoteActionable: "0.7" }), (defaults) => ({
|
|
131
|
+
flow: async () => {
|
|
132
|
+
await human("review-gate")
|
|
133
|
+
},
|
|
134
|
+
defaults: { ...defaults, reviewBase: "" },
|
|
135
|
+
doors: {
|
|
136
|
+
review: {
|
|
137
|
+
args: [{ name: "base", optional: true }],
|
|
138
|
+
vars: ({ base }) => (base === undefined ? {} : { reviewBase: base }),
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
base: (_workflow, vars) => vars.reviewBase || "",
|
|
142
|
+
}))
|
|
143
|
+
|
|
144
|
+
export const fix = defineWorkflow(settings({ judgeBudgetBytes: "65536" }), (defaults) => ({
|
|
145
|
+
flow: async () => {
|
|
146
|
+
await human("fix-precheck")
|
|
147
|
+
},
|
|
148
|
+
defaults,
|
|
149
|
+
doors: { fix: {} },
|
|
150
|
+
}))
|
|
151
|
+
|
|
152
|
+
export const rollout = defineWorkflow(settings({ flagRegistry: "flags.ts" }), (defaults) => ({
|
|
153
|
+
flow: async () => {
|
|
154
|
+
await human("flipping")
|
|
155
|
+
},
|
|
156
|
+
defaults: { ...defaults, flag: "" },
|
|
157
|
+
doors: { rollout: { args: [{ name: "flag" }], vars: ({ flag }) => ({ flag: flag ?? "" }) } },
|
|
158
|
+
}))
|
|
159
|
+
|
|
160
|
+
export const unwind = async () => {
|
|
161
|
+
await human("unwinding")
|
|
162
|
+
}
|
|
163
|
+
`
|
|
164
|
+
|
|
165
|
+
/** Writes `config` as gtd.config.ts beside {@link FACTORIES} as `workflows.ts`. */
|
|
166
|
+
const withFactories = (config: string): void => {
|
|
167
|
+
writeFileSync(join(projectDir, "workflows.ts"), FACTORIES)
|
|
168
|
+
writeFileSync(join(projectDir, "gtd.config.ts"), config)
|
|
169
|
+
}
|
|
170
|
+
|
|
75
171
|
beforeEach(() => {
|
|
76
172
|
homeDir = mkdtempSync(join(tmpdir(), "gtd-config-"))
|
|
77
173
|
projectDir = join(homeDir, "project")
|
|
78
174
|
mkdirSync(projectDir)
|
|
79
|
-
writeFileSync(join(projectDir, "gtd.config.ts"),
|
|
175
|
+
writeFileSync(join(projectDir, "gtd.config.ts"), BASE_CONFIG)
|
|
80
176
|
})
|
|
81
177
|
|
|
82
178
|
afterEach(() => {
|
|
@@ -97,11 +193,11 @@ const minimalWorkflow = (first: string) =>
|
|
|
97
193
|
const withConfig = (config: string) =>
|
|
98
194
|
writeFileSync(
|
|
99
195
|
join(projectDir, "gtd.config.ts"),
|
|
100
|
-
`${
|
|
196
|
+
`${BASE_CONFIG}\nexport const config = ${config}\n`,
|
|
101
197
|
)
|
|
102
198
|
|
|
103
199
|
describe("ConfigService", () => {
|
|
104
|
-
it("
|
|
200
|
+
it("a config runs its default workflow, its envDefaults as environment settings", async () => {
|
|
105
201
|
const cfg = await getConfig()
|
|
106
202
|
|
|
107
203
|
expect(cfg.workflow.initial).toBe("idle")
|
|
@@ -109,11 +205,11 @@ describe("ConfigService", () => {
|
|
|
109
205
|
expect(cfg.defaultWorkflow.vars["testCommand"]).toBeUndefined()
|
|
110
206
|
})
|
|
111
207
|
|
|
112
|
-
it("with no gtd.config.ts anywhere: fails,
|
|
208
|
+
it("with no gtd.config.ts anywhere: fails, pointing at gtd install", async () => {
|
|
113
209
|
rmSync(join(projectDir, "gtd.config.ts"))
|
|
114
210
|
|
|
115
211
|
await expect(getConfig()).rejects.toThrow(
|
|
116
|
-
/no workflows configured: no gtd\.config\.ts here or above —
|
|
212
|
+
/no workflows configured: no gtd\.config\.ts here or above — gtd ships no workflow; `gtd install` says where to get one/,
|
|
117
213
|
)
|
|
118
214
|
})
|
|
119
215
|
|
|
@@ -149,7 +245,7 @@ describe("ConfigService", () => {
|
|
|
149
245
|
const others = Layer.succeed(ConfigDiscovery, {
|
|
150
246
|
legacyRcFiles: () => Effect.succeed([{ path: "/shared/.gtdrc", owned: false }]),
|
|
151
247
|
workflowModule: () =>
|
|
152
|
-
Effect.succeed({ filepath: join(projectDir, "gtd.config.ts"), source:
|
|
248
|
+
Effect.succeed({ filepath: join(projectDir, "gtd.config.ts"), source: BASE_CONFIG }),
|
|
153
249
|
})
|
|
154
250
|
const cfg = await Effect.runPromise(
|
|
155
251
|
Effect.flatMap(ConfigService, (c) => c.load).pipe(
|
|
@@ -172,7 +268,7 @@ describe("ConfigService", () => {
|
|
|
172
268
|
it("refuses a user-level rc file in home, even when the walk never reaches home", async () => {
|
|
173
269
|
writeFileSync(join(homeDir, ".gtdrc.json"), `{ "$schema": "x" }\n`)
|
|
174
270
|
const outside = mkdtempSync(join(tmpdir(), "gtd-outside-"))
|
|
175
|
-
writeFileSync(join(outside, "gtd.config.ts"),
|
|
271
|
+
writeFileSync(join(outside, "gtd.config.ts"), BASE_CONFIG)
|
|
176
272
|
|
|
177
273
|
try {
|
|
178
274
|
await expect(getConfig()).rejects.toThrow(`${join(homeDir, ".gtdrc.json")}: no longer read`)
|
|
@@ -233,7 +329,7 @@ describe("ConfigService", () => {
|
|
|
233
329
|
join(projectDir, "gtd.config.ts"),
|
|
234
330
|
[
|
|
235
331
|
`import { defineConfig } from "@pmelab/gtd/flows"`,
|
|
236
|
-
|
|
332
|
+
BASE_CONFIG,
|
|
237
333
|
`export const config = defineConfig({ ui: { port: 4173 } })`,
|
|
238
334
|
``,
|
|
239
335
|
].join("\n"),
|
|
@@ -247,7 +343,7 @@ describe("ConfigService", () => {
|
|
|
247
343
|
join(projectDir, "gtd.config.ts"),
|
|
248
344
|
[
|
|
249
345
|
`import { defineConfig } from "@pmelab/gtd/flows"`,
|
|
250
|
-
|
|
346
|
+
BASE_CONFIG,
|
|
251
347
|
`export const config = defineConfig({ env: { testCommand: "x" } })`,
|
|
252
348
|
``,
|
|
253
349
|
].join("\n"),
|
|
@@ -318,7 +414,7 @@ describe("ConfigService", () => {
|
|
|
318
414
|
expect(Object.keys(cfg.fingerprint).sort()).toEqual(["gtd.config.ts", "steps.ts"])
|
|
319
415
|
})
|
|
320
416
|
|
|
321
|
-
it("the fingerprint covers gtd.config.ts but no
|
|
417
|
+
it("the fingerprint covers gtd.config.ts but no gtd module it imports", async () => {
|
|
322
418
|
const cfg = await getConfig()
|
|
323
419
|
|
|
324
420
|
expect(Object.keys(cfg.fingerprint)).toEqual(["gtd.config.ts"])
|
|
@@ -465,7 +561,8 @@ describe("ConfigService", () => {
|
|
|
465
561
|
const flow = (first: string) => `async () => {\n await human("${first}")\n}`
|
|
466
562
|
const header = `import { human } from "@pmelab/gtd/flows"\n`
|
|
467
563
|
|
|
468
|
-
it("
|
|
564
|
+
it("re-exported factories are startable under their names, default is not", async () => {
|
|
565
|
+
withFactories(`export { feature as default, feature, review, fix } from "./workflows"\n`)
|
|
469
566
|
const cfg = await getConfig()
|
|
470
567
|
expect(cfg.workflowNames).toEqual(["feature", "fix", "review"])
|
|
471
568
|
expect(cfg.defaultWorkflow.name).toBe("feature")
|
|
@@ -512,38 +609,12 @@ describe("ConfigService", () => {
|
|
|
512
609
|
expect((await getConfig()).defaultWorkflow.name).toBe("hotfix")
|
|
513
610
|
})
|
|
514
611
|
|
|
515
|
-
it("a helper re-exported from
|
|
516
|
-
|
|
517
|
-
join(projectDir, "gtd.config.ts"),
|
|
518
|
-
[`export { feature as default, unwind } from "@pmelab/gtd/workflows"`, ``].join("\n"),
|
|
519
|
-
)
|
|
612
|
+
it("a helper re-exported from a workflow module becomes a startable workflow", async () => {
|
|
613
|
+
withFactories(`export { feature as default, unwind } from "./workflows"\n`)
|
|
520
614
|
expect((await getConfig()).workflowNames).toContain("unwind")
|
|
521
615
|
})
|
|
522
616
|
|
|
523
|
-
it("
|
|
524
|
-
writeFileSync(
|
|
525
|
-
join(projectDir, "gtd.config.ts"),
|
|
526
|
-
[`export { feature as default, feature } from "@pmelab/gtd/workflow"`, ``].join("\n"),
|
|
527
|
-
)
|
|
528
|
-
expect((await getConfig()).defaultWorkflow.name).toBe("feature")
|
|
529
|
-
})
|
|
530
|
-
|
|
531
|
-
it("each shipped workflow has its own subpath", async () => {
|
|
532
|
-
writeFileSync(
|
|
533
|
-
join(projectDir, "gtd.config.ts"),
|
|
534
|
-
[
|
|
535
|
-
`export { feature as default, feature } from "@pmelab/gtd/workflows/feature"`,
|
|
536
|
-
`export { review } from "@pmelab/gtd/workflows/review"`,
|
|
537
|
-
`export { fix } from "@pmelab/gtd/workflows/fix"`,
|
|
538
|
-
``,
|
|
539
|
-
].join("\n"),
|
|
540
|
-
)
|
|
541
|
-
const config = await getConfig()
|
|
542
|
-
expect(config.defaultWorkflow.name).toBe("feature")
|
|
543
|
-
expect(config.workflowNames).toEqual(["feature", "fix", "review"])
|
|
544
|
-
})
|
|
545
|
-
|
|
546
|
-
it("a shipped subpath only a helper file imports resolves", async () => {
|
|
617
|
+
it("a gtd subpath only a helper file imports resolves", async () => {
|
|
547
618
|
writeFileSync(
|
|
548
619
|
join(projectDir, "kit.ts"),
|
|
549
620
|
[
|
|
@@ -598,7 +669,8 @@ describe("ConfigService", () => {
|
|
|
598
669
|
].join("\n"),
|
|
599
670
|
)
|
|
600
671
|
|
|
601
|
-
it("
|
|
672
|
+
it("exported factories bring their doors, review taking an optional base", async () => {
|
|
673
|
+
withFactories(`export { feature as default, review, fix } from "./workflows"\n`)
|
|
602
674
|
const { doors } = await getConfig()
|
|
603
675
|
expect([...doors.keys()].sort()).toEqual(["fix", "review"])
|
|
604
676
|
expect(doors.get("fix")?.workflow).toBe("fix")
|
|
@@ -606,44 +678,33 @@ describe("ConfigService", () => {
|
|
|
606
678
|
})
|
|
607
679
|
|
|
608
680
|
it("a config exporting only fix gets only fix's door", async () => {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
`export default ${flow("main")}`,
|
|
614
|
-
`export { fix } from "@pmelab/gtd/workflows"`,
|
|
615
|
-
``,
|
|
616
|
-
].join("\n"),
|
|
681
|
+
withFactories(
|
|
682
|
+
[header, `export default ${flow("main")}`, `export { fix } from "./workflows"`, ``].join(
|
|
683
|
+
"\n",
|
|
684
|
+
),
|
|
617
685
|
)
|
|
618
686
|
const { doors } = await getConfig()
|
|
619
687
|
expect([...doors.keys()]).toEqual(["fix"])
|
|
620
688
|
})
|
|
621
689
|
|
|
622
690
|
it("a door follows the name its workflow is exported under", async () => {
|
|
623
|
-
|
|
624
|
-
join(projectDir, "gtd.config.ts"),
|
|
625
|
-
`export { feature as default, review as audit } from "@pmelab/gtd/workflows"\n`,
|
|
626
|
-
)
|
|
691
|
+
withFactories(`export { feature as default, review as audit } from "./workflows"\n`)
|
|
627
692
|
const door = (await getConfig()).doors.get("review")
|
|
628
693
|
expect(door?.workflow).toBe("audit")
|
|
629
694
|
expect(door?.vars?.({ base: "main" })).toEqual({ reviewBase: "main" })
|
|
630
695
|
})
|
|
631
696
|
|
|
632
697
|
it("a review factory exported as audit reviews the default branch, or an explicit base", async () => {
|
|
633
|
-
|
|
634
|
-
join(projectDir, "gtd.config.ts"),
|
|
635
|
-
`export { feature as default, review as audit } from "@pmelab/gtd/workflows"\n`,
|
|
636
|
-
)
|
|
698
|
+
withFactories(`export { feature as default, review as audit } from "./workflows"\n`)
|
|
637
699
|
const audit = (await getConfig()).workflowNamed("audit")
|
|
638
700
|
expect(audit?.def.base?.("audit", {})).toBe("")
|
|
639
701
|
expect(audit?.def.base?.("audit", { reviewBase: "v1" })).toBe("v1")
|
|
640
702
|
})
|
|
641
703
|
|
|
642
704
|
it("a config's own base export wins over the review factory's", async () => {
|
|
643
|
-
|
|
644
|
-
join(projectDir, "gtd.config.ts"),
|
|
705
|
+
withFactories(
|
|
645
706
|
[
|
|
646
|
-
`export { feature as default, review } from "
|
|
707
|
+
`export { feature as default, review } from "./workflows"`,
|
|
647
708
|
`export const base = (w: string) => (w === "review" ? "develop" : undefined)`,
|
|
648
709
|
``,
|
|
649
710
|
].join("\n"),
|
|
@@ -652,8 +713,8 @@ describe("ConfigService", () => {
|
|
|
652
713
|
})
|
|
653
714
|
|
|
654
715
|
it("a door several workflows build gets no door when none is exported under its name, until the doors export declares it", async () => {
|
|
655
|
-
const both = `export { feature as default, fix as fix2, fix as fix3 } from "
|
|
656
|
-
|
|
716
|
+
const both = `export { feature as default, fix as fix2, fix as fix3 } from "./workflows"`
|
|
717
|
+
withFactories(`${both}\n`)
|
|
657
718
|
expect((await getConfig()).doors.get("fix")).toBeUndefined()
|
|
658
719
|
writeFileSync(
|
|
659
720
|
join(projectDir, "gtd.config.ts"),
|
|
@@ -663,9 +724,8 @@ describe("ConfigService", () => {
|
|
|
663
724
|
})
|
|
664
725
|
|
|
665
726
|
it("a door clash warns once at doors.<name>, builders sorted", async () => {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
`export { feature as default, fix as fix3, fix as fix4, fix as fix2 } from "@pmelab/gtd/workflows"\n`,
|
|
727
|
+
withFactories(
|
|
728
|
+
`export { feature as default, fix as fix3, fix as fix4, fix as fix2 } from "./workflows"\n`,
|
|
669
729
|
)
|
|
670
730
|
const cfg = await getConfig()
|
|
671
731
|
const clash = cfg.warnings.filter((w) => w.path[0] === "doors")
|
|
@@ -680,70 +740,38 @@ describe("ConfigService", () => {
|
|
|
680
740
|
})
|
|
681
741
|
|
|
682
742
|
it("no clash warning when an export is named after the door, one workflow builds it, or doors declares it", async () => {
|
|
683
|
-
const both = `export { feature as default, fix as fix2, fix as fix3 } from "
|
|
743
|
+
const both = `export { feature as default, fix as fix2, fix as fix3 } from "./workflows"`
|
|
684
744
|
for (const body of [
|
|
685
|
-
`export { feature as default, fix as fix2, fix } from "
|
|
686
|
-
`export { feature as default, fix } from "
|
|
745
|
+
`export { feature as default, fix as fix2, fix } from "./workflows"\n`,
|
|
746
|
+
`export { feature as default, fix } from "./workflows"\n`,
|
|
687
747
|
`${both}\nexport const doors = { fix: { workflow: "fix3" } }\n`,
|
|
688
748
|
]) {
|
|
689
|
-
|
|
749
|
+
withFactories(body)
|
|
690
750
|
expect((await getConfig()).warnings.filter((w) => w.path[0] === "doors")).toEqual([])
|
|
691
751
|
}
|
|
692
752
|
})
|
|
693
753
|
|
|
694
754
|
it("door ownership does not depend on export order", async () => {
|
|
695
|
-
|
|
696
|
-
join(projectDir, "gtd.config.ts"),
|
|
697
|
-
`export { feature as default, fix as fix2, fix } from "@pmelab/gtd/workflows"\n`,
|
|
698
|
-
)
|
|
755
|
+
withFactories(`export { feature as default, fix as fix2, fix } from "./workflows"\n`)
|
|
699
756
|
expect((await getConfig()).doors.get("fix")?.workflow).toBe("fix")
|
|
700
757
|
})
|
|
701
758
|
|
|
702
759
|
it("rollout and a second instance of it load, door rollout running workflow rollout whichever export comes first", async () => {
|
|
703
760
|
for (const body of [
|
|
704
|
-
`export { rollout } from "
|
|
705
|
-
`import { rollout } from "
|
|
761
|
+
`export { rollout } from "./workflows"\nimport { rollout } from "./workflows"\nexport const elsewhere = rollout({ flagRegistry: "x.ts" })`,
|
|
762
|
+
`import { rollout } from "./workflows"\nexport const elsewhere = rollout({ flagRegistry: "x.ts" })\nexport { rollout }`,
|
|
706
763
|
]) {
|
|
707
|
-
|
|
708
|
-
join(projectDir, "gtd.config.ts"),
|
|
709
|
-
`export { feature as default } from "@pmelab/gtd/workflows"\n${body}\n`,
|
|
710
|
-
)
|
|
764
|
+
withFactories(`export { feature as default } from "./workflows"\n${body}\n`)
|
|
711
765
|
expect((await getConfig()).doors.get("rollout")?.workflow).toBe("rollout")
|
|
712
766
|
}
|
|
713
767
|
})
|
|
714
768
|
|
|
715
|
-
it("shipped rollout and cleanup list their doors beside fix and review", async () => {
|
|
716
|
-
writeFileSync(
|
|
717
|
-
join(projectDir, "gtd.config.ts"),
|
|
718
|
-
`export { feature as default, fix, review, rollout, cleanup } from "@pmelab/gtd/workflows"\n`,
|
|
719
|
-
)
|
|
720
|
-
const { doors } = await getConfig()
|
|
721
|
-
expect([...doors.keys()]).toEqual(["cleanup", "fix", "review", "rollout"])
|
|
722
|
-
expect(doors.get("rollout")?.args).toEqual([{ name: "flag" }])
|
|
723
|
-
expect(doors.get("cleanup")?.args).toEqual([{ name: "flag" }])
|
|
724
|
-
})
|
|
725
|
-
|
|
726
|
-
it("rollout and cleanup load from their own subpaths", async () => {
|
|
727
|
-
writeFileSync(
|
|
728
|
-
join(projectDir, "gtd.config.ts"),
|
|
729
|
-
[
|
|
730
|
-
`export { feature as default, fix, review } from "@pmelab/gtd/workflows"`,
|
|
731
|
-
`export { rollout } from "@pmelab/gtd/workflows/rollout"`,
|
|
732
|
-
`export { cleanup } from "@pmelab/gtd/workflows/cleanup"`,
|
|
733
|
-
``,
|
|
734
|
-
].join("\n"),
|
|
735
|
-
)
|
|
736
|
-
const { doors } = await getConfig()
|
|
737
|
-
expect([...doors.keys()]).toEqual(["cleanup", "fix", "review", "rollout"])
|
|
738
|
-
expect(doors.get("rollout")?.args).toEqual([{ name: "flag" }])
|
|
739
|
-
expect(doors.get("cleanup")?.args).toEqual([{ name: "flag" }])
|
|
740
|
-
})
|
|
741
|
-
|
|
742
769
|
it("a config's doors merge derived doors (exported workflows) with declared doors (doors export)", async () => {
|
|
743
770
|
withDoors(
|
|
744
771
|
`{ hot: { workflow: "hotfix", args: [{ name: "ticket" }] } }`,
|
|
745
|
-
'export { fix } from "
|
|
772
|
+
'export { fix } from "./workflows"',
|
|
746
773
|
)
|
|
774
|
+
writeFileSync(join(projectDir, "workflows.ts"), FACTORIES)
|
|
747
775
|
const { doors } = await getConfig()
|
|
748
776
|
expect([...doors.keys()].sort()).toEqual(["fix", "hot"])
|
|
749
777
|
expect(doors.get("fix")).toBeDefined()
|
|
@@ -939,10 +967,7 @@ describe("ConfigService", () => {
|
|
|
939
967
|
|
|
940
968
|
describe("ConfigService — workflow factory options", () => {
|
|
941
969
|
const configWith = (body: string) =>
|
|
942
|
-
|
|
943
|
-
join(projectDir, "gtd.config.ts"),
|
|
944
|
-
[`import { feature, fix, review } from "@pmelab/gtd/workflows"`, body, ``].join("\n"),
|
|
945
|
-
)
|
|
970
|
+
withFactories([`import { feature, fix, review } from "./workflows"`, body, ``].join("\n"))
|
|
946
971
|
|
|
947
972
|
it("an option given in gtd.config.ts becomes the workflow's setting of its kind", async () => {
|
|
948
973
|
configWith(
|