@pmelab/gtd 59.0.0 → 60.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +42 -38
  3. package/claude/hooks/doors.ts +8 -4
  4. package/dist/chunks/flows-DENt0uWV.mjs +707 -0
  5. package/dist/chunks/{steering-CCNvngSH.mjs → steering-DF_BMadH.mjs} +292 -289
  6. package/dist/chunks/{testkit-CMBAAyRK.mjs → testkit-BvecVjQ0.mjs} +937 -772
  7. package/dist/chunks/{wire-C12dahrH.mjs → wire-DZ8R1RU9.mjs} +9 -2
  8. package/dist/flows.mjs +2 -676
  9. package/dist/gtd.bundle.mjs +1101 -1025
  10. package/dist/steering.mjs +2 -2
  11. package/dist/testkit/gherkin.mjs +371 -82
  12. package/dist/testkit.mjs +3 -2
  13. package/dist/web/main.js +234 -232
  14. package/dist/wire.mjs +2 -2
  15. package/dist/workflow-kit.mjs +3 -3
  16. package/package.json +1 -1
  17. package/skills/authoring/SKILL.md +68 -61
  18. package/src/cli/Cli.test.ts +311 -229
  19. package/src/cli/Cli.ts +486 -585
  20. package/src/cli/Help.test.ts +47 -0
  21. package/src/cli/Help.ts +70 -0
  22. package/src/cli/History.ts +9 -4
  23. package/src/cli/Judges.ts +1 -1
  24. package/src/cli/Output.test.ts +35 -0
  25. package/src/cli/Output.ts +31 -1
  26. package/src/cli/Program.test.ts +131 -82
  27. package/src/cli/Program.ts +197 -111
  28. package/src/edge/Edge.test.ts +64 -56
  29. package/src/edge/Edge.ts +4 -4
  30. package/src/emit/Emit.test.ts +1 -1
  31. package/src/emit/Emit.ts +1 -1
  32. package/src/flows/factory.test.ts +14 -0
  33. package/src/flows/factory.ts +25 -6
  34. package/src/flows/helpers.test.ts +35 -0
  35. package/src/flows/helpers.ts +19 -10
  36. package/src/flows/index.test.ts +66 -0
  37. package/src/flows/index.ts +109 -5
  38. package/src/flows/internal.ts +7 -0
  39. package/src/flows/runtime.test.ts +192 -1
  40. package/src/flows/runtime.ts +188 -54
  41. package/src/gherkin/git-control.feature +6 -6
  42. package/src/gherkin/index.test.ts +127 -0
  43. package/src/gherkin/index.ts +481 -98
  44. package/src/gherkin/messages.test.ts +124 -0
  45. package/src/gherkin/messages.ts +101 -0
  46. package/src/gherkin/more-steps.feature +170 -0
  47. package/src/gherkin/scenario-end.feature +101 -0
  48. package/src/gherkin/testkit.feature +83 -7
  49. package/src/install/Install.test.ts +7 -7
  50. package/src/install/Install.ts +8 -8
  51. package/src/replay/Replay.test.ts +13 -2
  52. package/src/replay/Replay.ts +19 -30
  53. package/src/replay/judgeDiffShape.test.ts +1 -1
  54. package/src/steering/Footnotes.test.ts +1 -1
  55. package/src/steering/SteeringFormats.test.ts +1 -1
  56. package/src/steering/freeform.test.ts +1 -1
  57. package/src/steering/index.test.ts +27 -1
  58. package/src/steering/index.ts +6 -104
  59. package/src/steering/internal.ts +11 -0
  60. package/src/steering/qa.test.ts +7 -2
  61. package/src/steering/reads.test.ts +42 -0
  62. package/src/steering/reads.ts +43 -0
  63. package/src/steering/registry.ts +93 -0
  64. package/src/steering/review.test.ts +2 -2
  65. package/src/step/planStart.test.ts +18 -16
  66. package/src/step/planStart.ts +9 -9
  67. package/src/testkit/TestRepo.test.ts +289 -6
  68. package/src/testkit/TestRepo.ts +317 -41
  69. package/src/testkit/imports.test.ts +58 -0
  70. package/src/testkit/imports.ts +102 -0
  71. package/src/testkit/index.test.ts +28 -0
  72. package/src/testkit/index.ts +14 -2
  73. package/src/twins/EmittedScriptRecognizer.ts +7 -14
  74. package/src/twins/EmittedScripts.test.ts +4 -2
  75. package/src/twins/InMemRepo.ts +1 -0
  76. package/src/wire/Land.ts +1 -1
  77. package/src/wire/Script.ts +11 -0
  78. package/src/wire/WireSchema.test.ts +3 -0
  79. package/src/wire/golden/1/script.json +4 -0
  80. package/src/wire/index.ts +1 -0
  81. package/src/workflow/SteeringFormats.test.ts +1 -1
  82. package/src/workflow/compile.ts +2 -2
  83. package/src/workflow/discovery.test.ts +3 -3
  84. package/src/workflow/load.test.ts +256 -116
  85. package/src/workflow/load.ts +175 -152
  86. package/src/workflow/skills.test.ts +5 -5
  87. package/src/workflow/skills.ts +1 -1
  88. package/src/workflow-kit/index.test.ts +25 -0
  89. package/src/workflow-kit/index.ts +37 -4
  90. package/src/workflow-kit/review.ts +2 -2
@@ -0,0 +1,28 @@
1
+ import { afterEach, describe, expect, it } from "vitest"
2
+ import { agent, read } from "../flows/index.js"
3
+ import { installContext, type FlowContext, type StepRequest } from "./index.js"
4
+
5
+ afterEach(() => installContext(undefined))
6
+
7
+ describe("the flow-context harness", () => {
8
+ it("renders a prompt and records a step against an installed context", async () => {
9
+ const requests: StepRequest[] = []
10
+ installContext({
11
+ read: (path: string) => (path === "PLAN.md" ? "add a button" : undefined),
12
+ step: async (request: StepRequest) => {
13
+ requests.push(request)
14
+ },
15
+ } as Partial<FlowContext> as FlowContext)
16
+
17
+ await agent("build", `Build: ${read("PLAN.md")}`)
18
+
19
+ expect(requests).toEqual([
20
+ { kind: "agent", name: "build", prompt: "Build: add a button", options: {} },
21
+ ])
22
+ })
23
+
24
+ it("leaves a flow failing outside a replay once uninstalled", () => {
25
+ installContext(undefined)
26
+ expect(() => read("PLAN.md")).toThrow("called outside a gtd replay")
27
+ })
28
+ })
@@ -2,15 +2,27 @@
2
2
  // repository. It imports no test runner, so any runner can use it; the
3
3
  // gherkin steps are the opt-in \`@pmelab/gtd/testkit/gherkin\`.
4
4
  export {
5
+ inTurn,
5
6
  runsItsScript,
6
7
  testRepo,
7
- type Actor,
8
+ verdict,
9
+ verdictWhen,
10
+ whenPrompted,
8
11
  type Actors,
9
12
  type CannedCommand,
10
13
  type DriveOptions,
14
+ type RunScriptOptions,
15
+ type ScriptedActor,
16
+ type TestCommit,
11
17
  type TestRepo,
12
18
  type TestRepoOptions,
19
+ type Verdict,
20
+ type VerdictEntry,
13
21
  type WorkflowModule,
14
22
  } from "./TestRepo.js"
15
- export { InMemRepo, type CapturedCliResult } from "../twins/index.js"
23
+ export type { BeatDocument } from "../wire/index.js"
24
+ export type { CapturedCliResult } from "../twins/index.js"
16
25
  export type { TestkitWorld } from "../gherkin/index.js"
26
+ // The flow-context harness: render a prompt or stub a step synchronously,
27
+ // against a replay context the test installs instead of the engine.
28
+ export { installContext, type FlowContext, type StepRequest } from "../flows/internal.js"
@@ -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,
@@ -31,7 +30,7 @@ import {
31
30
  formatFinding,
32
31
  steeringFormatFor,
33
32
  type SteeringFormat,
34
- } from "../steering/index.js"
33
+ } from "../steering/internal.js"
35
34
  import { refNameFault } from "../replay/index.js"
36
35
  import { renderFormat } from "../wire/index.js"
37
36
  import { applyFlowScript, rewritten } from "./FlowScripts.js"
@@ -519,12 +518,8 @@ const recognizeGtdCheck = (repo: InMemRepo, block: string): BlockOutcome | undef
519
518
  const known = BUILT_IN_MODE_NAMES.join(", ")
520
519
  return { kind: "failed", error: `gtd check: unknown mode "${mode}" — known modes: ${known}` }
521
520
  }
522
- // An ABSENT file has nothing to check and exits 0 — `runCheckCommand`'s own
523
- // documented behavior. Validating `""` instead would report every "missing
524
- // header" finding the format has, turning "the reviewer deleted the file"
525
- // (a case the review-doc guard owns) into a script failure.
526
521
  const content = repo.readFile(file)
527
- if (content === undefined) return { kind: "noop" }
522
+ if (content === undefined) return { kind: "failed", error: `gtd check: no such file: ${file}` }
528
523
  const findings = format.validate(content).map((finding) => formatFinding(file, finding))
529
524
  if (findings.length === 0) return { kind: "noop" }
530
525
  return {
@@ -552,13 +547,11 @@ const recognizeGtdUncheck = (repo: InMemRepo, block: string): BlockOutcome | und
552
547
  }
553
548
 
554
549
  /**
555
- * `Emit.ts`'s `combinedScript` leading comment — `gtd land`/`gtd --workflow`'s
556
- * whole plain-text artifact opens with this line ahead of the required
557
- * script, so recognizing it as a no-op (like the retry-helper function
558
- * definition) keeps the `@inmem` tier green.
550
+ * A block of nothing but comments is a no-op in `sh` too — `combinedScript`'s
551
+ * leading `DID_NOT_RUN_COMMENT`, and the list of paths a start commits.
559
552
  */
560
- const recognizeDidNotRunComment = (block: string): BlockOutcome | undefined =>
561
- block === DID_NOT_RUN_COMMENT ? { kind: "noop" } : undefined
553
+ const recognizeCommentBlock = (block: string): BlockOutcome | undefined =>
554
+ block.split("\n").every((line) => line.startsWith("#")) ? { kind: "noop" } : undefined
562
555
 
563
556
  /** `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
557
  const PRESENTATION_FAILURE_TEXT = `${extractQuotedTokens(PRESENTATION_FAILURE_WARNING)[0]!.replace(/\\n$/, "")}\n`
@@ -695,7 +688,7 @@ const recognizersFor = (
695
688
  git: boolean,
696
689
  ): ReadonlyArray<(block: string) => BlockOutcome | undefined> => [
697
690
  (block) => recognizeSetFlags(block),
698
- (block) => recognizeDidNotRunComment(block),
691
+ (block) => recognizeCommentBlock(block),
699
692
  (block) => recognizePrecondition(repo, block),
700
693
  (block) => recognizeFileExistsGuard(repo, block),
701
694
  (block) => recognizeBinaryGuard(commands, block),
@@ -140,8 +140,10 @@ for (const makeTier of gitTiers) {
140
140
  })
141
141
 
142
142
  describe("gtd check", () => {
143
- it("passes a valid file, and an absent one", () => {
144
- expect(run(script(`gtd check qa '${FILE}'`)).ok).toBe(true)
143
+ it("refuses an absent file, and passes a valid one", () => {
144
+ const absent = run(script(`gtd check qa '${FILE}'`))
145
+ expect(absent).toMatchObject({ ok: false, status: 1 })
146
+ expect(absent.output).toContain(`gtd check: no such file: ${FILE}`)
145
147
  t.seed.writeFile(FILE, "## Open Questions\n")
146
148
  expect(run(script(`gtd check qa '${FILE}'`))).toEqual({ ok: true, status: 0, output: "" })
147
149
  })
@@ -30,6 +30,7 @@ function makeHash(message: string, parent: string | null, tree: Map<string, stri
30
30
  export class InMemRepo {
31
31
  // Data-only marker read by no code path: embeds the sentinel in every
32
32
  // instance so a leaked object still carries it.
33
+ // fallow-ignore-next-line unused-class-member
33
34
  readonly testDouble = TEST_DOUBLE_SENTINEL
34
35
 
35
36
  private commits: Map<string, Commit> = new Map()
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(run \`gtd ${command} --json=script | sh\` to get the landing script)\n`
74
+ ? `${landProseText(fields.subject).trimEnd()}\n(to land it: s=$(gtd ${command} --json=script) && sh -c "$s")\n`
75
75
  : noopText(fields.state)
@@ -0,0 +1,11 @@
1
+ import { WIRE_SCHEMA } from "./constants.js"
2
+
3
+ /** `--json` of every command that prints a script and nothing else to decide on: abandon, restore, start, door, validate, history fetch. */
4
+ export interface ScriptFields {
5
+ readonly schema: number
6
+ readonly script: string
7
+ }
8
+
9
+ export const scriptFields = (script: string): ScriptFields => ({ schema: WIRE_SCHEMA, script })
10
+
11
+ export const renderScriptJson = (fields: ScriptFields): string => JSON.stringify(fields) + "\n"
@@ -9,6 +9,8 @@ import {
9
9
  landFields,
10
10
  renderBeatJson,
11
11
  renderLandJson,
12
+ renderScriptJson,
13
+ scriptFields,
12
14
  statusOf,
13
15
  WIRE_SCHEMA,
14
16
  type BeatKind,
@@ -116,6 +118,7 @@ const documents: Readonly<Record<string, string>> = {
116
118
  }),
117
119
  ),
118
120
  "judge.json": JUDGE,
121
+ "script.json": renderScriptJson(scriptFields("git reset --mixed ...\n")),
119
122
  }
120
123
 
121
124
  describe(`wire schema ${WIRE_SCHEMA}`, () => {
@@ -0,0 +1,4 @@
1
+ {
2
+ "schema": 1,
3
+ "script": "git reset --mixed ...\n"
4
+ }
package/src/wire/index.ts CHANGED
@@ -6,6 +6,7 @@ export * from "./BeatStatus.js"
6
6
  export * from "./BeatDocument.js"
7
7
  export * from "./BeatPlain.js"
8
8
  export * from "./Land.js"
9
+ export * from "./Script.js"
9
10
  export * from "./Judge.js"
10
11
  export { selectPath } from "./Select.js"
11
12
  export { resolveSession } from "./Sessions.js"
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it } from "vitest"
2
2
  import fc from "fast-check"
3
3
  import { isSeededValidateCommand, seededValidateCommand } from "./SteeringFormats.js"
4
- import { BUILT_IN_MODE_NAMES } from "../steering/index.js"
4
+ import { BUILT_IN_MODE_NAMES } from "../steering/internal.js"
5
5
 
6
6
  describe("seededValidateCommand / isSeededValidateCommand", () => {
7
7
  it("recognizes its own seeded command for every built-in mode", () => {
@@ -1,7 +1,7 @@
1
1
  import { seededValidateCommand } from "./SteeringFormats.js"
2
- import { BUILT_IN_MODE_NAMES } from "../steering/index.js"
2
+ import { BUILT_IN_MODE_NAMES } from "../steering/internal.js"
3
3
  import type { ModeDef } from "./Workflow.js"
4
- import { configProblems, type GtdConfig, type UiSettings } from "../flows/index.js"
4
+ import { configProblems, type GtdConfig, type UiSettings } from "../flows/internal.js"
5
5
  import type { Diagnostic } from "./Diagnostic.js"
6
6
 
7
7
  export interface CompiledConfig {
@@ -22,15 +22,15 @@ interface Tier {
22
22
 
23
23
  const config = (imports: string): string =>
24
24
  [
25
- 'import { agent, human } from "@pmelab/gtd/flows"',
25
+ 'import { agent, human, workflow } from "@pmelab/gtd/flows"',
26
26
  imports,
27
27
  "export default async () => {",
28
28
  ' await human("idle", { message: "go" })',
29
29
  ' await agent("working", brief)',
30
30
  "}",
31
- "export const named = async () => {",
31
+ "export const named = workflow(async () => {",
32
32
  ' await agent("working", brief)',
33
- "}",
33
+ "})",
34
34
  "",
35
35
  ].join("\n")
36
36