@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,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
+ })
@@ -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
+ }
@@ -3,7 +3,13 @@ import { historyTrail } from "../edge/index.js"
3
3
  import { combinedScript, fetchHistoryRefs } from "../emit/index.js"
4
4
  import { GitService, Narrator, PROCESS_HISTORY_PREFIX } from "../platform/index.js"
5
5
  import { parseCommitMessage } from "../replay/index.js"
6
- import { writeJson, type ArtifactOut, type CommandRequirements, type JsonMode } from "./Output.js"
6
+ import {
7
+ writeJson,
8
+ writeScript,
9
+ type ArtifactOut,
10
+ type CommandRequirements,
11
+ type JsonMode,
12
+ } from "./Output.js"
7
13
 
8
14
  export interface HistoryEntry {
9
15
  readonly ref: string
@@ -138,8 +144,7 @@ export const runHistoryShowCommand = (
138
144
  /** Prints the script and never runs git: gtd's promise that it only decides and prints. */
139
145
  export const runHistoryFetchCommand = (
140
146
  remote: string,
147
+ json: JsonMode,
141
148
  out: ArtifactOut,
142
149
  ): Effect.Effect<void, Error, CommandRequirements> =>
143
- Effect.sync(() => {
144
- out.write(`${combinedScript(fetchHistoryRefs(remote), "")}\n`)
145
- })
150
+ writeScript(json, out, `${combinedScript(fetchHistoryRefs(remote), "")}\n`)
package/src/cli/Judges.ts CHANGED
@@ -2,7 +2,7 @@ import { Effect, Option } from "effect"
2
2
  import { answered, numeric } from "../flows/index.js"
3
3
  import { GitService } from "../platform/index.js"
4
4
  import { parseCommitMessage, type JudgeVerdict } from "../replay/index.js"
5
- import { headingSectionBodies } from "../steering/index.js"
5
+ import { headingSectionBodies } from "../steering/internal.js"
6
6
  import { selectHistoryEntries, type HistoryEntry } from "./History.js"
7
7
  import { printable } from "./Metrics.js"
8
8
  import { writeJson, type ArtifactOut, type CommandRequirements, type JsonMode } from "./Output.js"
@@ -0,0 +1,35 @@
1
+ import { Effect, Exit } from "effect"
2
+ import { describe, expect, it } from "vitest"
3
+ import { WIRE_SCHEMA } from "../wire/index.js"
4
+ import { writeScript, type JsonMode } from "./Output.js"
5
+
6
+ const run = (json: JsonMode, script: string, plain?: string) => {
7
+ let written = ""
8
+ const out = { write: (chunk: string) => (written += chunk), flush: () => {} }
9
+ const exit = Effect.runSyncExit(writeScript(json, out, script, plain))
10
+ return { exit, written }
11
+ }
12
+
13
+ describe("writeScript", () => {
14
+ it("plain prints the script, or the plain text when given", () => {
15
+ expect(run({ kind: "off" }, "git reset\n").written).toBe("git reset\n")
16
+ expect(run({ kind: "off" }, "", "nothing to do\n").written).toBe("nothing to do\n")
17
+ })
18
+
19
+ it("--json prints { schema, script }, the script ending in exactly one newline", () => {
20
+ expect(JSON.parse(run({ kind: "document" }, "git reset\n\n").written)).toEqual({
21
+ schema: WIRE_SCHEMA,
22
+ script: "git reset\n",
23
+ })
24
+ expect(JSON.parse(run({ kind: "document" }, "", "nothing\n").written)).toEqual({
25
+ schema: WIRE_SCHEMA,
26
+ script: "",
27
+ })
28
+ })
29
+
30
+ it("--json=script prints the script alone; an unknown path fails", () => {
31
+ expect(run({ kind: "select", path: "script" }, "git reset").written).toBe("git reset\n\n")
32
+ expect(run({ kind: "select", path: "schema" }, "x").written).toBe(`${WIRE_SCHEMA}\n`)
33
+ expect(Exit.isFailure(run({ kind: "select", path: "nope" }, "x").exit)).toBe(true)
34
+ })
35
+ })
package/src/cli/Output.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import { Effect } from "effect"
2
2
  import { CommandRunner, GitService, Host, Narrator, Workspace } from "../platform/index.js"
3
- import { selectPath, type EXIT_OK, type EXIT_STRAYS_FOUND } from "../wire/index.js"
3
+ import {
4
+ renderScriptJson,
5
+ scriptFields,
6
+ selectPath,
7
+ type EXIT_OK,
8
+ type EXIT_STRAYS_FOUND,
9
+ } from "../wire/index.js"
4
10
  import { ConfigDiscovery, ConfigService } from "../workflow/index.js"
5
11
  import type { ToolCommands } from "./ToolCommands.js"
6
12
 
@@ -81,3 +87,27 @@ export const writeJson = (
81
87
  else yield* writeSelection(out, document, json.path)
82
88
  return true
83
89
  })
90
+
91
+ /** Exactly one trailing newline: a script embedded in a `--json` document is never re-normalized at its tail. */
92
+ export const normalizeScriptNewline = (script: string): string =>
93
+ script.length === 0 ? script : `${script.replace(/\n+$/, "")}\n`
94
+
95
+ /**
96
+ * A script-printing command's output: the script itself (or `plain`, when
97
+ * there is none to run), `{ schema, script }` under `--json`, or one field.
98
+ */
99
+ export const writeScript = (
100
+ json: JsonMode,
101
+ out: ArtifactOut,
102
+ script: string,
103
+ plain: string = script,
104
+ ): Effect.Effect<void, Error> =>
105
+ Effect.gen(function* () {
106
+ if (json.kind === "off") {
107
+ out.write(plain)
108
+ return
109
+ }
110
+ const fields = scriptFields(normalizeScriptNewline(script))
111
+ if (json.kind === "document") out.write(renderScriptJson(fields))
112
+ else yield* writeSelection(out, fields, json.path)
113
+ })