@pmelab/gtd 52.1.0 → 53.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 +91 -129
  3. package/claude/hooks/drive.ts +0 -3
  4. package/claude/hooks/register.tsx +3 -68
  5. package/claude/hooks/ship.ts +85 -1595
  6. package/dist/chunks/{reserved-DxB5CROj.mjs → reserved-CQ7amcKG.mjs} +45 -132
  7. package/dist/chunks/{review-CKMSdvgW.mjs → review-CNgN_V8T.mjs} +2 -2
  8. package/dist/chunks/{tail-QZc7Uz2U.mjs → tail-JZ9PAOwp.mjs} +2 -14
  9. package/dist/chunks/{wire-BJuZWR3C.mjs → wire-C12dahrH.mjs} +2 -130
  10. package/dist/flows.mjs +1 -16
  11. package/dist/gtd.bundle.mjs +476 -982
  12. package/dist/testkit.mjs +58 -417
  13. package/dist/wire.mjs +2 -2
  14. package/dist/workflows/cleanup.mjs +2 -2
  15. package/dist/workflows/feature.mjs +112 -363
  16. package/dist/workflows/fix.mjs +2 -2
  17. package/dist/workflows/review.mjs +3 -3
  18. package/dist/workflows/rollout.mjs +1 -1
  19. package/dist/workflows.mjs +311 -674
  20. package/package.json +1 -1
  21. package/src/cli/Cli.test.ts +1 -86
  22. package/src/cli/Cli.ts +13 -133
  23. package/src/cli/Metrics.test.ts +21 -31
  24. package/src/cli/Metrics.ts +25 -36
  25. package/src/cli/Program.test.ts +1 -35
  26. package/src/cli/Program.ts +5 -64
  27. package/src/cli/ToolCommands.ts +0 -3
  28. package/src/edge/Edge.test.ts +20 -322
  29. package/src/edge/Edge.ts +10 -181
  30. package/src/edge/index.ts +0 -1
  31. package/src/flows/runtime.test.ts +0 -27
  32. package/src/flows/runtime.ts +1 -49
  33. package/src/install/Install.ts +15 -24
  34. package/src/replay/Replay.test.ts +0 -169
  35. package/src/replay/Replay.ts +4 -93
  36. package/src/replay/index.ts +0 -1
  37. package/src/testkit/Layers.ts +0 -1
  38. package/src/wire/BeatDocument.ts +2 -12
  39. package/src/wire/BeatStatus.ts +0 -3
  40. package/src/wire/WireSchema.test.ts +0 -18
  41. package/src/wire/index.ts +0 -2
  42. package/src/wire/types.ts +0 -7
  43. package/src/workflow/Workflow.ts +1 -9
  44. package/src/workflows/feature/criteria.test.ts +1 -14
  45. package/src/workflows/feature/criteria.ts +6 -38
  46. package/src/workflows/feature/feature.ts +34 -118
  47. package/src/workflows/feature/hands-on-skip.feature +2 -70
  48. package/src/workflows/feature/index.ts +0 -1
  49. package/src/workflows/feature/one-step-land.feature +0 -4
  50. package/src/workflows/feature/prototype-rounds.feature +1 -1
  51. package/src/workflows/feature/prototype.feature +3 -3
  52. package/src/workflows/feature/prototype.ts +18 -29
  53. package/src/workflows/feature/refactor-absorbs.feature +3 -8
  54. package/src/workflows/feature/refactor-first.feature +0 -5
  55. package/src/workflows/feature/refactor-rebuild.feature +0 -6
  56. package/src/workflows/feature/refactor-review.feature +11 -11
  57. package/src/workflows/feature/refactor.ts +30 -64
  58. package/src/workflows/feature/review-gate.feature +1 -1
  59. package/src/workflows/feature/seam-rejection.feature +0 -4
  60. package/src/workflows/feature/seam.test.ts +7 -26
  61. package/src/workflows/feature/seam.ts +52 -165
  62. package/src/workflows/index.ts +0 -1
  63. package/src/workflows/shared/lenses.ts +0 -13
  64. package/src/workflows/shared/options.test.ts +2 -11
  65. package/src/workflows/shared/options.ts +4 -21
  66. package/src/workflows/shared/review.test.ts +3 -3
  67. package/src/workflows/shared/steps.ts +0 -2
  68. package/src/workflows/shared/text.test.ts +10 -80
  69. package/src/workflows/shared/text.ts +35 -211
  70. package/src/workflows/shared/vars.ts +2 -8
  71. package/src/workflows/shared/workflows.steps.ts +0 -13
  72. package/claude/hooks/context.ts +0 -66
  73. package/src/replay/Slices.test.ts +0 -73
  74. package/src/replay/Slices.ts +0 -45
  75. package/src/wire/ContextLimit.fixture.ts +0 -30
  76. package/src/wire/ContextLimit.test.ts +0 -96
  77. package/src/wire/ContextLimit.ts +0 -169
  78. package/src/wire/Slices.ts +0 -25
  79. package/src/wire/golden/1/slices.json +0 -11
  80. package/src/workflows/feature/context-limit.feature +0 -37
  81. package/src/workflows/feature/continuation-cap.feature +0 -69
  82. package/src/workflows/feature/continuation-rebuild.feature +0 -92
  83. package/src/workflows/feature/continuation-slices.feature +0 -68
  84. package/src/workflows/feature/continuation.feature +0 -209
  85. package/src/workflows/feature/slice-findings.feature +0 -256
  86. package/src/workflows/feature/slicing-cap.feature +0 -23
  87. package/src/workflows/feature/slicing.feature +0 -297
  88. package/src/workflows/feature/slicing.test.ts +0 -49
  89. package/src/workflows/feature/slicing.ts +0 -105
  90. package/src/workflows/shared/continuation.ts +0 -92
@@ -1,45 +0,0 @@
1
- export const IMPLICIT_SLICE = "main"
2
-
3
- /** Same shape as mark()'s name rule; kept local so @pmelab/gtd/flows publishes no regex. */
4
- export const SLICE_NAME = /^[a-z][a-z0-9-]*$/
5
-
6
- /** One slice() call as replay saw it. Hashes are the commit replay stood on; "" is a pending turn. */
7
- export interface SliceSpan {
8
- readonly name: string
9
- readonly enteredAt: string
10
- /** undefined: the flow never left it — the process rests inside, or restart() fired inside. */
11
- readonly leftAt: string | undefined
12
- }
13
-
14
- export interface SliceRange {
15
- readonly name: string
16
- /** Exclusive: the range is base..tip in git's sense. */
17
- readonly base: string
18
- readonly tip: string
19
- /** The tip can still move: only the last range, since a rest is never past the episode's end. */
20
- readonly open: boolean
21
- }
22
-
23
- /**
24
- * Folds boundaries into ranges: each kept span's base is the previous kept
25
- * span's `leftAt`, so a gap before a span belongs to it, and the last span
26
- * runs to the episode tip. A closed span that landed nothing is dropped.
27
- */
28
- export const sliceRanges = (
29
- spans: readonly SliceSpan[],
30
- episode: { readonly base: string; readonly tip: string },
31
- ): readonly SliceRange[] => {
32
- if (spans.some((s) => s.enteredAt === "" || s.leftAt === "")) {
33
- throw new Error('sliceRanges: a pending turn\'s "" hash is not a commit')
34
- }
35
- const kept = spans.filter((s) => s.leftAt === undefined || s.enteredAt !== s.leftAt)
36
- if (kept.length === 0) {
37
- return [{ name: IMPLICIT_SLICE, base: episode.base, tip: episode.tip, open: true }]
38
- }
39
- return kept.map((s, i) => ({
40
- name: s.name,
41
- base: i === 0 ? episode.base : (kept[i - 1]!.leftAt ?? episode.base),
42
- tip: i === kept.length - 1 ? episode.tip : (s.leftAt ?? episode.tip),
43
- open: i === kept.length - 1,
44
- }))
45
- }
@@ -1,30 +0,0 @@
1
- /** A Claude Code transcript whose latest assistant message used `tokens` of context, as the hook reads it from `transcript_path`. */
2
- export const transcriptAt = (tokens: number): string =>
3
- [
4
- { type: "user", message: { role: "user", content: "build it" } },
5
- {
6
- type: "assistant",
7
- message: {
8
- id: "msg_1",
9
- role: "assistant",
10
- usage: { input_tokens: 3, cache_read_input_tokens: 10, cache_creation_input_tokens: 7 },
11
- },
12
- },
13
- { type: "user", message: { role: "user", content: [{ type: "tool_result" }] } },
14
- {
15
- type: "assistant",
16
- message: {
17
- id: "msg_2",
18
- role: "assistant",
19
- usage: {
20
- input_tokens: 1,
21
- cache_read_input_tokens: tokens - 1001,
22
- cache_creation_input_tokens: 1000,
23
- output_tokens: 500,
24
- },
25
- },
26
- },
27
- { type: "attachment", attachment: { type: "hook_success" } },
28
- ]
29
- .map((entry) => JSON.stringify(entry))
30
- .join("\n") + "\n"
@@ -1,96 +0,0 @@
1
- import { describe, expect, it } from "vitest"
2
- import {
3
- CONTEXT_LIMIT_MARKER,
4
- DEFAULT_CONTEXT_LIMIT,
5
- contextHook,
6
- contextLimitFault,
7
- contextWindowFor,
8
- hookOutput,
9
- subagentTranscriptPath,
10
- transcriptTokens,
11
- } from "./ContextLimit.js"
12
- import { transcriptAt } from "./ContextLimit.fixture.js"
13
-
14
- const at = (tokens: number, event: "PostToolUse" | "PreCompact" = "PostToolUse") =>
15
- contextHook({
16
- event,
17
- tokens: transcriptTokens(transcriptAt(tokens)),
18
- window: 200_000,
19
- limit: DEFAULT_CONTEXT_LIMIT,
20
- })
21
-
22
- describe("transcriptTokens", () => {
23
- it("counts the latest assistant message's input, cache-read and cache-creation tokens", () => {
24
- expect(transcriptTokens(transcriptAt(50_000))).toBe(50_000)
25
- })
26
-
27
- it("is undefined for a transcript with no assistant usage yet", () => {
28
- expect(transcriptTokens('{"type":"user"}\nnot json\n')).toBeUndefined()
29
- })
30
- })
31
-
32
- describe("contextHook", () => {
33
- it("lets a run below the soft limit go on", () => {
34
- expect(at(119_999)).toBeUndefined()
35
- })
36
-
37
- it("blocks with a handoff-note request at the soft limit", () => {
38
- const decision = at(120_000)
39
- expect(decision?.kind).toBe("soft")
40
- expect(hookOutput(decision!)).toEqual({
41
- decision: "block",
42
- reason: expect.stringContaining("short handoff note"),
43
- })
44
- })
45
-
46
- it("stops the run at the hard limit, behind the marker", () => {
47
- const decision = at(140_000)
48
- expect(hookOutput(decision!)).toEqual({
49
- continue: false,
50
- stopReason: expect.stringMatching(new RegExp(`^${CONTEXT_LIMIT_MARKER}: 140000 of 200000`)),
51
- })
52
- })
53
-
54
- it("stops the run on PreCompact, whatever the count", () => {
55
- expect(hookOutput(at(1_000, "PreCompact")!)).toEqual({
56
- continue: false,
57
- stopReason: expect.stringContaining(
58
- `${CONTEXT_LIMIT_MARKER}: the context is about to be compacted`,
59
- ),
60
- })
61
- })
62
-
63
- it("lets a run with no usage yet go on", () => {
64
- const limit = DEFAULT_CONTEXT_LIMIT
65
- expect(
66
- contextHook({ event: "PostToolUse", tokens: undefined, window: 1, limit }),
67
- ).toBeUndefined()
68
- })
69
- })
70
-
71
- describe("contextLimitFault", () => {
72
- it("accepts shares in (0, 1] with soft below hard", () => {
73
- expect(contextLimitFault({ soft: 0.5, hard: 1 })).toBeUndefined()
74
- expect(contextLimitFault({ hard: 0.9 })).toBeUndefined()
75
- })
76
-
77
- it("refuses a share outside (0, 1] and a soft limit at or above the hard one", () => {
78
- expect(contextLimitFault({ soft: 0 })).toMatch(/context\.soft must be/)
79
- expect(contextLimitFault({ hard: 70 })).toMatch(/context\.hard must be/)
80
- expect(contextLimitFault({ soft: 0.8 })).toMatch(/below context\.hard \(0\.7\)/)
81
- const misspelt: { hard: number } = Object.assign({ hard: 0.8 }, { sotf: 0.5 })
82
- expect(contextLimitFault(misspelt)).toBe("unknown key(s) sotf in context")
83
- })
84
- })
85
-
86
- it("reads the long window from Claude Code's [1m] suffix only", () => {
87
- expect(contextWindowFor("opus[1m]")).toBe(1_000_000)
88
- expect(contextWindowFor("opus")).toBe(200_000)
89
- expect(contextWindowFor(undefined)).toBe(200_000)
90
- })
91
-
92
- it("finds a worker's own transcript beside the main session's", () => {
93
- expect(subagentTranscriptPath("/p/session.jsonl", "a1")).toBe(
94
- "/p/session/subagents/agent-a1.jsonl",
95
- )
96
- })
@@ -1,169 +0,0 @@
1
- // The context-limit decision every driver shares: `gtd context-hook`
2
- // and the Claude Code mod both answer a hook event through `contextHook`, so
3
- // the two cannot drift on thresholds, wording or which tokens count.
4
-
5
- /** A share of the model's context window, each in (0, 1]; `soft` below `hard`. */
6
- export interface ContextLimit {
7
- readonly soft: number
8
- readonly hard: number
9
- }
10
-
11
- export const DEFAULT_CONTEXT_LIMIT: ContextLimit = { soft: 0.6, hard: 0.7 }
12
-
13
- const DEFAULT_WINDOW = 200_000
14
- const LONG_WINDOW = 1_000_000
15
-
16
- /** Claude Code's `[1m]` suffix selects the long window; a transcript never names it, so the driver decides from the model it passed. */
17
- export const contextWindowFor = (model: string | undefined): number =>
18
- model !== undefined && /\[1m\]/i.test(model) ? LONG_WINDOW : DEFAULT_WINDOW
19
-
20
- /** A positive whole number of tokens, or `undefined` for anything else. */
21
- export const parseContextWindow = (raw: string | undefined): number | undefined => {
22
- const n = Number(raw)
23
- return raw !== undefined && raw.trim() !== "" && Number.isInteger(n) && n > 0 ? n : undefined
24
- }
25
-
26
- /** `gtd context-hook`'s request: the hook's stdin and the limits its flags set. */
27
- export interface ContextHookRequest {
28
- readonly input: string
29
- readonly soft?: number
30
- readonly hard?: number
31
- readonly window?: number
32
- }
33
-
34
- /**
35
- * Where a driver asks `gtd context-hook` to record a hard stop's `stopReason`:
36
- * Claude Code prints it nowhere a driver reads, and `hook_stopped` alone would
37
- * also name any other hook that stopped the turn.
38
- */
39
- export const CONTEXT_STOP_FILE_ENV = "GTD_LOOP_CONTEXT_STOP_FILE"
40
-
41
- /**
42
- * Why one layer of a limit (a scope's, a step's) is malformed, ignoring how its
43
- * two shares relate: layers merge key by key, so only the merged limit has to
44
- * keep `soft` below `hard`. Config is evaluated untyped, so anything may arrive.
45
- */
46
- export const contextShapeFault = (limit: unknown): string | undefined => {
47
- if (typeof limit !== "object" || limit === null || Array.isArray(limit)) {
48
- return `context must be an object { soft, hard }, got ${JSON.stringify(limit)}`
49
- }
50
- const unknown = Object.keys(limit).filter((key) => key !== "soft" && key !== "hard")
51
- if (unknown.length > 0) return `unknown key(s) ${unknown.join(", ")} in context`
52
- const shares = limit as { readonly soft?: unknown; readonly hard?: unknown }
53
- for (const key of ["soft", "hard"] as const) {
54
- const value = shares[key]
55
- if (value === undefined) continue
56
- if (typeof value !== "number" || !(value > 0 && value <= 1)) {
57
- return `context.${key} must be a share of the context window in (0, 1], got ${JSON.stringify(value)}`
58
- }
59
- }
60
- return undefined
61
- }
62
-
63
- /** Why a whole limit is unusable, or `undefined` when it is fine. */
64
- export const contextLimitFault = (limit: unknown): string | undefined => {
65
- const shape = contextShapeFault(limit)
66
- if (shape !== undefined) return shape
67
- const shares = limit as { readonly soft?: unknown; readonly hard?: unknown }
68
- const soft = (shares.soft as number | undefined) ?? DEFAULT_CONTEXT_LIMIT.soft
69
- const hard = (shares.hard as number | undefined) ?? DEFAULT_CONTEXT_LIMIT.hard
70
- return soft < hard ? undefined : `context.soft (${soft}) must be below context.hard (${hard})`
71
- }
72
-
73
- const record = (value: unknown): Record<string, unknown> | undefined =>
74
- typeof value === "object" && value !== null ? (value as Record<string, unknown>) : undefined
75
-
76
- const count = (value: unknown): number => (typeof value === "number" && value > 0 ? value : 0)
77
-
78
- /** What a message's usage puts in the context window: its whole input, cached or not. */
79
- export const usageTokens = (usage: unknown): number | undefined => {
80
- const u = record(usage)
81
- if (u === undefined) return undefined
82
- return (
83
- count(u["input_tokens"]) +
84
- count(u["cache_read_input_tokens"]) +
85
- count(u["cache_creation_input_tokens"])
86
- )
87
- }
88
-
89
- /**
90
- * The context size of the latest assistant message in a Claude Code transcript
91
- * (one JSON entry per line). Usage is per message, not a running sum, so the
92
- * last entry alone is the window's fill.
93
- */
94
- export const transcriptTokens = (transcript: string): number | undefined => {
95
- const lines = transcript.split("\n")
96
- for (let i = lines.length - 1; i >= 0; i--) {
97
- const line = lines[i]!.trim()
98
- if (line === "") continue
99
- let entry: unknown
100
- try {
101
- entry = JSON.parse(line)
102
- } catch {
103
- continue
104
- }
105
- if (record(entry)?.["type"] !== "assistant") continue
106
- const tokens = usageTokens(record(record(entry)?.["message"])?.["usage"])
107
- if (tokens !== undefined) return tokens
108
- }
109
- return undefined
110
- }
111
-
112
- /**
113
- * A worker subagent's own transcript. A hook its tool call fires is handed the
114
- * MAIN session's `transcript_path`; the worker's sits beside it.
115
- */
116
- export const subagentTranscriptPath = (transcriptPath: string, agentId: string): string =>
117
- `${transcriptPath.replace(/\.jsonl$/, "")}/subagents/agent-${agentId}.jsonl`
118
-
119
- /** Every hard stop's `stopReason` starts with this: the driver's context-limit marker. */
120
- export const CONTEXT_LIMIT_MARKER = "gtd: context limit"
121
-
122
- export type ContextEvent = "PostToolUse" | "PreCompact"
123
-
124
- export type ContextDecision =
125
- | { readonly kind: "soft"; readonly reason: string }
126
- | { readonly kind: "hard"; readonly stopReason: string }
127
-
128
- const percent = (share: number): string => `${Math.round(share * 100)}%`
129
-
130
- /**
131
- * What a hook answers, or `undefined` to let the run go on. Compaction about
132
- * to happen means the limit was missed, so `PreCompact` is a hard stop whatever
133
- * the count says.
134
- */
135
- export const contextHook = (input: {
136
- readonly event: ContextEvent
137
- readonly tokens: number | undefined
138
- readonly window: number
139
- readonly limit: ContextLimit
140
- }): ContextDecision | undefined => {
141
- const { event, tokens, window, limit } = input
142
- if (event === "PreCompact") {
143
- return {
144
- kind: "hard",
145
- stopReason: `${CONTEXT_LIMIT_MARKER}: the context is about to be compacted — the run is too big for one turn`,
146
- }
147
- }
148
- if (tokens === undefined) return undefined
149
- const used = `${tokens} of ${window} tokens`
150
- if (tokens >= limit.hard * window) {
151
- return {
152
- kind: "hard",
153
- stopReason: `${CONTEXT_LIMIT_MARKER}: ${used}, past the ${percent(limit.hard)} hard limit`,
154
- }
155
- }
156
- if (tokens >= limit.soft * window) {
157
- return {
158
- kind: "soft",
159
- reason: `Context limit: ${used} used, past the ${percent(limit.soft)} soft limit. Stop working now. Do not start anything new; end your turn with a short handoff note as your answer: what you touched, and what was next. Where your instructions name a file for that note, write it there too.`,
160
- }
161
- }
162
- return undefined
163
- }
164
-
165
- /** A decision as Claude Code's settings-hook JSON output. */
166
- export const hookOutput = (decision: ContextDecision): Record<string, unknown> =>
167
- decision.kind === "soft"
168
- ? { decision: "block", reason: decision.reason }
169
- : { continue: false, stopReason: decision.stopReason }
@@ -1,25 +0,0 @@
1
- import { WIRE_SCHEMA } from "./constants.js"
2
-
3
- export interface SliceEntry {
4
- readonly name: string
5
- /** Full hash, exclusive: git's base..tip. */
6
- readonly base: string
7
- /** Full hash, inclusive; HEAD's hash for the open slice. */
8
- readonly tip: string
9
- /** false only for the last slice of an underway process. */
10
- readonly closed: boolean
11
- }
12
-
13
- export interface SlicesDocument {
14
- readonly schema: number
15
- readonly slices: readonly SliceEntry[]
16
- }
17
-
18
- /** The ONLY place `gtd slices --json`'s shape is built; copies exactly the four fields, in order. */
19
- export const slicesDocument = (slices: readonly SliceEntry[]): SlicesDocument => ({
20
- schema: WIRE_SCHEMA,
21
- slices: slices.map(({ name, base, tip, closed }) => ({ name, base, tip, closed })),
22
- })
23
-
24
- export const renderSlicesJson = (document: SlicesDocument): string =>
25
- JSON.stringify(document) + "\n"
@@ -1,11 +0,0 @@
1
- {
2
- "schema": 1,
3
- "slices": [
4
- {
5
- "name": "build",
6
- "base": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
7
- "tip": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
8
- "closed": true
9
- }
10
- ]
11
- }
@@ -1,37 +0,0 @@
1
- @inmem
2
- Feature: the shipped workflows take a context limit as an option
3
-
4
- `contextLimit: { soft, hard }` is a factory option: every agent step of the
5
- workflow runs under it, carried as `gtd next --json`'s `context`. Without it a
6
- step carries none and the driver's defaults apply.
7
-
8
- Scenario Outline: every agent step carries the configured limit, or none
9
- Given a gtd config with:
10
- """
11
- import { feature } from "@pmelab/gtd/workflows/feature"
12
- export { summary, steering, skills, access } from "@pmelab/gtd/workflows/feature"
13
- export default feature(<options>)
14
- """
15
- And a file "src/calc.ts" with:
16
- """
17
- export const add = (a: number, b: number) => a + b
18
- """
19
- And the working tree is committed as "feat: add calculator"
20
- And a file "TODO.md" with:
21
- """
22
- # Add subtract
23
-
24
- subtract(a, b) returns the difference.
25
- """
26
- And step "unwind" changes nothing
27
- And step "start-gate.check" changes nothing
28
- When the process is driven
29
- Then the process rests at "design.triage"
30
- And the output of gtd "next --json" <carries>
31
-
32
- Examples:
33
- | options | carries |
34
- | { contextLimit: { soft: 0.5, hard: 0.65 } } | contains "\"context\":{\"soft\":0.5,\"hard\":0.65}" |
35
- | { contextLimit: { hard: 0.8 } } | contains "\"context\":{\"hard\":0.8}" |
36
- | {} | does not contain "\"context\"" |
37
-
@@ -1,69 +0,0 @@
1
- @inmem
2
- Feature: past its continuation cap a phase rests for a person
3
-
4
- With `continuations` at 1, a naive pass stopped at its context limit gets
5
- one continuation; when that one stops too, `seam.oversized` asks a person,
6
- and landing there grants a fresh round.
7
-
8
- Background:
9
- Given the shipped workflows, as gtd install scaffolds them
10
- And an environment variable "GTD_CONTINUATIONS" set to "1"
11
- And an environment variable "GTD_QUALITYREVIEWS" set to ""
12
- And an environment variable "GTD_REFACTORQUALITYREVIEWS" set to ""
13
- And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
14
- And a file "src/calc.ts" with:
15
- """
16
- export const add = (a: number, b: number) => a + b
17
- """
18
- And the working tree is committed as "feat: add calculator"
19
- And a file "TODO.md" with:
20
- """
21
- # Add subtract and multiply
22
- """
23
- And step "unwind" changes nothing
24
- And step "start-gate.check" changes nothing
25
- And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
26
- """
27
- # Requirements
28
-
29
- ## Impact
30
-
31
- - the greeting module
32
-
33
- ## Acceptance Criteria
34
-
35
- - AC-1: `subtract(5, 3)` returns 2.
36
- - AC-2: `multiply(2, 3)` returns 6.
37
- """
38
- And step "design.gate.answer" changes nothing
39
- And step "seam.check" changes nothing
40
-
41
- Scenario: past the continuation cap the naive pass rests for a person, and landing there grants a new round
42
- Given step "seam.naive" writes ".gtd/HANDOFF.md" with:
43
- """
44
- Roughed out subtract.
45
- """
46
- And step "seam.continue.naive" writes "tests/calc.test.ts" with:
47
- """
48
- it("[AC-1] subtracts", () => {})
49
- """
50
- And step "seam.continue.naive" writes ".gtd/HANDOFF.md" with:
51
- """
52
- Subtract is tested; multiply was next.
53
- """
54
- When the process is driven
55
- Then the process rests at "seam.oversized"
56
- And the resting step's content contains "1 continuation ran and the last still stopped at its context limit"
57
- And the resting step's content contains "still open: AC-2"
58
-
59
- When gtd lands
60
- Then the process rests at "seam.continue.naive"
61
- And the resting step's content contains "Still open: AC-2."
62
- And the commit subjects end with:
63
- """
64
- gtd(human): design.gate.answer → seam.naive
65
- gtd(agent): seam.naive → seam.continue.naive
66
- gtd(agent): seam.continue.naive → seam.oversized
67
- gtd(human): seam.oversized → seam.continue.naive
68
- """
69
-
@@ -1,92 +0,0 @@
1
- @inmem
2
- Feature: a rebuild stopped at its context limit is continued on the branch
3
-
4
- The rebuild's snapshot is its own commit: the continuation finds the work
5
- in the tree, and the health loop greens the whole build afterwards.
6
- `seamAllow` is empty here, so the naive pass's change to an existing test
7
- file is the judge's only stray and refactor-first rebuilds with no refactor.
8
-
9
- Background:
10
- Given the shipped workflows, as gtd install scaffolds them
11
- And an environment variable "GTD_SEAMALLOW" set to ""
12
- And an environment variable "GTD_QUALITYREVIEWS" set to ""
13
- And an environment variable "GTD_REFACTORQUALITYREVIEWS" set to ""
14
- And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
15
- And a file "src/calc.ts" with:
16
- """
17
- export const add = (a: number, b: number) => a + b
18
- """
19
- And a file "tests/calc.test.ts" with:
20
- """
21
- it("adds", () => {})
22
- """
23
- And the working tree is committed as "feat: add calculator"
24
- And a file "TODO.md" with:
25
- """
26
- # Add subtract and multiply
27
- """
28
- And step "unwind" changes nothing
29
- And step "start-gate.check" changes nothing
30
- And step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
31
- """
32
- # Requirements
33
-
34
- ## Impact
35
-
36
- - the greeting module
37
-
38
- ## Acceptance Criteria
39
-
40
- - AC-1: `subtract(5, 3)` returns 2.
41
- - AC-2: `multiply(2, 3)` returns 6.
42
- """
43
- And step "design.gate.answer" changes nothing
44
- And step "seam.check" changes nothing
45
-
46
- Scenario: a rebuild stopped at its context limit lands its snapshot and is continued on the branch
47
- Given step "seam.naive" writes "src/ops.ts" with:
48
- """
49
- export const subtract = (a: number, b: number) => a - b
50
- export const multiply = (a: number, b: number) => a * b
51
- """
52
- And step "seam.naive" writes "tests/calc.test.ts" with:
53
- """
54
- it("adds", () => {})
55
- it("[AC-1] subtracts", () => {})
56
- it("[AC-2] multiplies", () => {})
57
- """
58
- And judge step "seam.judge" answers:
59
- """
60
- [{"id":"shape","answer":"refactor-first","p":0.9}]
61
- """
62
- And step "implement.building" writes "tests/calc.test.ts" with:
63
- """
64
- it("adds", () => {})
65
- it("[AC-1] subtracts", () => {})
66
- """
67
- And step "implement.building" writes ".gtd/HANDOFF.md" with:
68
- """
69
- Rebuilt subtract behind the flag; multiply was next.
70
- """
71
- When the process is driven
72
- Then the process rests at "implement.continue.building"
73
- And the resting step's content contains "Still open: AC-2."
74
- And the resting step's content contains "Its work is already in the tree."
75
- And the resting step's content contains "Delete `.gtd/HANDOFF.md` once you are done"
76
- And "tests/calc.test.ts" contains "[AC-1] subtracts"
77
-
78
- Given step "implement.continue.building" writes "tests/calc.test.ts" with:
79
- """
80
- it("adds", () => {})
81
- it("[AC-1] subtracts", () => {})
82
- it("[AC-2] multiplies", () => {})
83
- """
84
- And step "implement.continue.building" deletes ".gtd/HANDOFF.md"
85
- When the process is driven
86
- Then the process rests at "implement.health.check"
87
- And the commit subjects end with:
88
- """
89
- gtd(agent): implement.building → implement.continue.building
90
- gtd(agent): implement.continue.building → implement.health.check
91
- """
92
-
@@ -1,68 +0,0 @@
1
- @inmem
2
- Feature: a continuation stays inside its coarse slice
3
-
4
- A naive pass of one coarse slice that stops at its context limit is
5
- continued with that slice's criteria still open, never another slice's.
6
-
7
- Background:
8
- Given the shipped workflows, as gtd install scaffolds them
9
- And an environment variable "GTD_QUALITYREVIEWS" set to ""
10
- And an environment variable "GTD_FLAGMODE" set to "false"
11
- And an environment variable "GTD_SLICEMAXCRITERIA" set to "2"
12
- And a file "src/calc.ts" with:
13
- """
14
- export const add = (a: number, b: number) => a + b
15
- """
16
- And the working tree is committed as "feat: add calculator"
17
- And a file "TODO.md" with:
18
- """
19
- # More arithmetic
20
-
21
- subtract, multiply and divide.
22
- """
23
- And step "unwind" changes nothing
24
- And step "start-gate.check" changes nothing
25
- And step "design.gate.answer" changes nothing
26
- And step "seam.check" changes nothing
27
-
28
- Scenario: the first slice's continuation is told only that slice's open criterion
29
- Given step "design.triage" writes ".gtd/REQUIREMENTS.md" with:
30
- """
31
- # Requirements
32
-
33
- ## Impact
34
-
35
- - the greeting module
36
-
37
- ## Acceptance Criteria
38
-
39
- - AC-1: subtract(a, b) returns a - b.
40
- - AC-2: multiply(a, b) returns a * b.
41
- - AC-3: divide(a, b) returns a / b.
42
- """
43
- And step "design.slicing" writes ".gtd/SLICES.md" with:
44
- """
45
- # Slices
46
-
47
- ## Subtract and divide
48
-
49
- - AC-1
50
- - AC-3
51
-
52
- ## Multiply
53
-
54
- - AC-2
55
- """
56
- And step "seam.naive" writes "src/sub.test.ts" with:
57
- """
58
- it("[AC-1] subtracts", () => {})
59
- """
60
- And step "seam.naive" writes ".gtd/HANDOFF.md" with:
61
- """
62
- Subtract is tested; divide was next.
63
- """
64
- When the process is driven
65
- Then the process rests at "seam.continue.naive"
66
- And the resting step's content contains "This slice builds AC-1, AC-3 only"
67
- And the resting step's content contains "Still open: AC-3."
68
- And the resting step's content does not contain "AC-2"