@pmelab/gtd 38.0.0 → 38.2.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 (39) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +36 -3
  3. package/dist/chunks/{Schema-DO5ttJaI.mjs → Schema-C6fLlQzx.mjs} +1 -1
  4. package/dist/chunks/{reserved-sfMdy7v7.mjs → reserved-C3PgP7XB.mjs} +56 -12
  5. package/dist/chunks/{tail-Cgca5Gp-.mjs → tail-Bkxki2nK.mjs} +1 -1
  6. package/dist/chunks/{wire-CSsa3Wxd.mjs → wire-BJuZWR3C.mjs} +1 -1
  7. package/dist/gtd.bundle.mjs +292 -53
  8. package/dist/testkit.mjs +257 -21
  9. package/dist/wire.mjs +1 -1
  10. package/dist/workflows/cleanup.mjs +2 -2
  11. package/dist/workflows/feature.mjs +158 -42
  12. package/dist/workflows/fix.mjs +2 -2
  13. package/dist/workflows/review.mjs +2 -2
  14. package/dist/workflows/rollout.mjs +1 -1
  15. package/dist/workflows.mjs +230 -70
  16. package/package.json +1 -1
  17. package/src/cli/Cli.test.ts +15 -3
  18. package/src/cli/Cli.ts +61 -28
  19. package/src/cli/History.ts +1 -19
  20. package/src/cli/Metrics.test.ts +224 -0
  21. package/src/cli/Metrics.ts +310 -0
  22. package/src/cli/Output.ts +13 -0
  23. package/src/cli/Program.test.ts +3 -1
  24. package/src/cli/Program.ts +4 -0
  25. package/src/testkit/steps.ts +4 -0
  26. package/src/wire/ContextLimit.ts +1 -1
  27. package/src/workflows/feature/continuation-cap.feature +65 -0
  28. package/src/workflows/feature/continuation-rebuild.feature +88 -0
  29. package/src/workflows/feature/continuation-slices.feature +64 -0
  30. package/src/workflows/feature/continuation.feature +205 -0
  31. package/src/workflows/feature/criteria.ts +11 -0
  32. package/src/workflows/feature/refactor.ts +64 -30
  33. package/src/workflows/feature/seam.ts +102 -30
  34. package/src/workflows/shared/continuation.ts +92 -0
  35. package/src/workflows/shared/options.ts +2 -0
  36. package/src/workflows/shared/steps.ts +1 -0
  37. package/src/workflows/shared/text.test.ts +6 -2
  38. package/src/workflows/shared/text.ts +104 -8
  39. package/src/workflows/shared/vars.ts +5 -0
@@ -0,0 +1,310 @@
1
+ import { Effect, Option } from "effect"
2
+ import { GitService } from "../platform/index.js"
3
+ import { parseCommitMessage } from "../replay/index.js"
4
+ import { UNATTRIBUTED_MODEL } from "../wire/index.js"
5
+ import { historyEntries, resolveHistoryRef, type HistoryEntry } from "./History.js"
6
+ import { writeJson, type ArtifactOut, type CommandRequirements, type JsonMode } from "./Output.js"
7
+
8
+ // Read from commit subjects and trailers alone, never by replay: a metric over
9
+ // old histories must not refuse because today's config has moved on. The step
10
+ // names below are the bundled feature workflow's public ones.
11
+
12
+ const QUALITY = ".gtd/QUALITY.md"
13
+
14
+ export interface MetricCommit {
15
+ readonly message: string
16
+ /** Findings this commit added to the quality lap's file; only a reviewing turn's count is read. */
17
+ readonly findings: number
18
+ }
19
+
20
+ export interface Metrics {
21
+ readonly slices: {
22
+ readonly decided: number
23
+ readonly landed: number
24
+ readonly rebuilt: number
25
+ readonly refactored: number
26
+ }
27
+ /** `rebuilt / decided`, to three places; null without a decision. */
28
+ readonly refactorFirstRate: number | null
29
+ /** Continuation turns per phase, keyed by the step without its `continue` segment. */
30
+ readonly continuations: Readonly<Record<string, number>>
31
+ readonly cost: {
32
+ readonly total: number
33
+ readonly byModel: Readonly<Record<string, number>>
34
+ readonly byPhase: {
35
+ readonly naive: number
36
+ readonly refactor: number
37
+ readonly implement: number
38
+ readonly other: number
39
+ }
40
+ /** A rebuilt slice's naive pass and the implement turns that rebuilt it. */
41
+ readonly doubleWork: {
42
+ readonly naive: number
43
+ readonly rebuild: number
44
+ readonly total: number
45
+ }
46
+ }
47
+ readonly findings: { readonly total: number; readonly byLens: Readonly<Record<string, number>> }
48
+ }
49
+
50
+ type Phase = keyof Metrics["cost"]["byPhase"]
51
+
52
+ // Keyed by the lap's path without `.reviewing`, so the refactor lap's lenses stay apart from the feature's.
53
+ const LENS = /^((?:.*\.)?quality\.[^.]+)\.reviewing$/
54
+
55
+ const CONTINUE = /(^|\.)continue\./
56
+
57
+ const inSeam = (step: string): boolean => step.startsWith("seam.")
58
+
59
+ // A naive retry after a continuation runs under `seam.fresh.naive`: any seam step named `naive` is the pass.
60
+ const phaseOf = (step: string): Phase => {
61
+ const scope = step.split(".")[0]
62
+ if (inSeam(step) && step.endsWith(".naive")) return "naive"
63
+ if (step === "seam.note" || scope === "refactor") return "refactor"
64
+ if (scope === "implement") return "implement"
65
+ return "other"
66
+ }
67
+
68
+ const round = (n: number, places = 6): number => {
69
+ const f = 10 ** places
70
+ return Math.round(n * f) / f
71
+ }
72
+
73
+ // Keys are model, step and lens names from history: a prototype-free record keeps `constructor` a plain key.
74
+ const record = (): Record<string, number> => Object.create(null) as Record<string, number>
75
+
76
+ const add = (into: Record<string, number>, key: string, n: number): void => {
77
+ into[key] = (into[key] ?? 0) + n
78
+ }
79
+
80
+ const roundAll = (record: Record<string, number>): Record<string, number> =>
81
+ Object.fromEntries(Object.entries(record).map(([k, v]) => [k, round(v)]))
82
+
83
+ interface Tally {
84
+ readonly slices: { decided: number; landed: number; rebuilt: number; refactored: number }
85
+ readonly continuations: Record<string, number>
86
+ readonly byModel: Record<string, number>
87
+ readonly byPhase: Record<Phase, number>
88
+ readonly doubleWork: { naive: number; rebuild: number }
89
+ readonly byLens: Record<string, number>
90
+ }
91
+
92
+ const emptyTally = (): Tally => ({
93
+ slices: { decided: 0, landed: 0, rebuilt: 0, refactored: 0 },
94
+ continuations: record(),
95
+ byModel: record(),
96
+ byPhase: { naive: 0, refactor: 0, implement: 0, other: 0 },
97
+ doubleWork: { naive: 0, rebuild: 0 },
98
+ byLens: record(),
99
+ })
100
+
101
+ const sum = (record: Readonly<Record<string, number>>): number =>
102
+ Object.values(record).reduce((a, b) => a + b, 0)
103
+
104
+ const finish = (t: Tally): Metrics => ({
105
+ slices: t.slices,
106
+ refactorFirstRate: t.slices.decided === 0 ? null : round(t.slices.rebuilt / t.slices.decided, 3),
107
+ continuations: t.continuations,
108
+ cost: {
109
+ total: round(sum(t.byModel)),
110
+ byModel: roundAll(t.byModel),
111
+ byPhase: roundAll(t.byPhase) as Metrics["cost"]["byPhase"],
112
+ doubleWork: {
113
+ naive: round(t.doubleWork.naive),
114
+ rebuild: round(t.doubleWork.rebuild),
115
+ total: round(t.doubleWork.naive + t.doubleWork.rebuild),
116
+ },
117
+ },
118
+ findings: { total: sum(t.byLens), byLens: t.byLens },
119
+ })
120
+
121
+ /** The open slice: its naive cost so far, and whether its implement turns rebuild it. */
122
+ interface Slice {
123
+ naive: number
124
+ rebuilding: boolean
125
+ /** Landed, not yet applied, with its naive cost: an apply that fails turns it into a rebuild. */
126
+ applying: number | undefined
127
+ }
128
+
129
+ const tallyCost = (
130
+ t: Tally,
131
+ open: Slice,
132
+ costs: ReturnType<typeof parseCommitMessage>["cost"],
133
+ phase: Phase,
134
+ ): void => {
135
+ for (const { cost, model } of costs) {
136
+ add(t.byModel, model ?? UNATTRIBUTED_MODEL, cost)
137
+ t.byPhase[phase] += cost
138
+ if (phase === "naive") open.naive += cost
139
+ if (phase === "implement" && open.rebuilding) t.doubleWork.rebuild += cost
140
+ }
141
+ }
142
+
143
+ /** A slice is decided where its turns leave the seam: for `implement.applying` the naive pass landed — unless it fails to apply and is built afresh — anywhere else it is rebuilt. */
144
+ const tallyDecision = (t: Tally, open: Slice, at: string, to: string): void => {
145
+ // The slice's closing ends its rebuild: later implement turns (an amend after review) are new work.
146
+ if (at === "implement.closing") open.rebuilding = false
147
+ if (open.applying !== undefined && at === "implement.applying") {
148
+ const naive = open.applying
149
+ open.applying = undefined
150
+ if (to !== "implement.building") return
151
+ t.slices.landed--
152
+ rebuild(t, open, naive)
153
+ return
154
+ }
155
+ if (!inSeam(at) || inSeam(to)) return
156
+ t.slices.decided++
157
+ const naive = open.naive
158
+ open.naive = 0
159
+ if (to === "implement.applying") {
160
+ open.applying = naive
161
+ open.rebuilding = false
162
+ t.slices.landed++
163
+ return
164
+ }
165
+ rebuild(t, open, naive)
166
+ if (at === "seam.note") t.slices.refactored++
167
+ }
168
+
169
+ const rebuild = (t: Tally, open: Slice, naive: number): void => {
170
+ open.rebuilding = true
171
+ t.slices.rebuilt++
172
+ t.doubleWork.naive += naive
173
+ }
174
+
175
+ /** Oldest first; a commit gtd did not write is skipped. */
176
+ export const processMetrics = (commits: readonly MetricCommit[]): Metrics => {
177
+ const t = emptyTally()
178
+ const open: Slice = { naive: 0, rebuilding: false, applying: undefined }
179
+ for (const commit of commits) {
180
+ const message = parseCommitMessage(commit.message)
181
+ const subject = message.parsed
182
+ if (subject === undefined) continue
183
+ const at = subject.from ?? subject.to
184
+ tallyCost(t, open, message.cost, phaseOf(at))
185
+ if (subject.actor === "agent" && CONTINUE.test(at))
186
+ add(t.continuations, at.replace(CONTINUE, "$1"), 1)
187
+ const lens = LENS.exec(at)?.[1]
188
+ if (lens !== undefined && commit.findings > 0) add(t.byLens, lens, commit.findings)
189
+ tallyDecision(t, open, at, subject.to)
190
+ }
191
+ return finish(t)
192
+ }
193
+
194
+ export const totalMetrics = (all: readonly Metrics[]): Metrics & { readonly processes: number } => {
195
+ const t = emptyTally()
196
+ for (const m of all) {
197
+ for (const key of Object.keys(t.slices) as (keyof Tally["slices"])[])
198
+ t.slices[key] += m.slices[key]
199
+ for (const [k, v] of Object.entries(m.continuations)) add(t.continuations, k, v)
200
+ for (const [k, v] of Object.entries(m.cost.byModel)) add(t.byModel, k, v)
201
+ for (const key of Object.keys(t.byPhase) as Phase[]) t.byPhase[key] += m.cost.byPhase[key]
202
+ t.doubleWork.naive += m.cost.doubleWork.naive
203
+ t.doubleWork.rebuild += m.cost.doubleWork.rebuild
204
+ for (const [k, v] of Object.entries(m.findings.byLens)) add(t.byLens, k, v)
205
+ }
206
+ return { processes: all.length, ...finish(t) }
207
+ }
208
+
209
+ const chunks = (content: string | undefined): number =>
210
+ content === undefined ? 0 : (content.match(/^## /gm) ?? []).length
211
+
212
+ export interface ProcessMetrics extends Metrics {
213
+ /** The opening commit's `Gtd-Workflow`; null for the default workflow. */
214
+ readonly workflow: string | null
215
+ }
216
+
217
+ export const readProcessMetrics = (
218
+ entry: Pick<HistoryEntry, "entry" | "tip">,
219
+ ): Effect.Effect<ProcessMetrics, Error, GitService> =>
220
+ Effect.gen(function* () {
221
+ const git = yield* GitService
222
+ const base = Option.getOrUndefined(yield* git.readRefOption(`${entry.entry}^`))
223
+ const commits = yield* git.commitHistory(base, entry.tip)
224
+ const quality = (hash: string) =>
225
+ git.readFileAtRef(hash, QUALITY).pipe(
226
+ Effect.map((content): string | undefined => content),
227
+ Effect.orElseSucceed(() => undefined),
228
+ )
229
+ // Only a commit that touched the file changes it: carry the last content forward.
230
+ let count = chunks(base === undefined ? undefined : yield* quality(base))
231
+ const read: MetricCommit[] = []
232
+ for (const { hash, message, touched } of commits) {
233
+ const before = count
234
+ if (touched.includes(QUALITY)) count = chunks(yield* quality(hash))
235
+ read.push({ message, findings: Math.max(0, count - before) })
236
+ }
237
+ const workflow =
238
+ commits[0] === undefined ? undefined : parseCommitMessage(commits[0].message).workflow
239
+ return { workflow: workflow ?? null, ...processMetrics(read) }
240
+ })
241
+
242
+ // Keys come from fetched history: never let them put control bytes on a terminal.
243
+ // eslint-disable-next-line no-control-regex -- the control range IS the point
244
+ const printable = (s: string): string => s.replace(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/g, "?")
245
+
246
+ const listed = (record: Readonly<Record<string, number>>): string =>
247
+ Object.entries(record)
248
+ .map(([k, v]) => `${printable(k)} ${v}`)
249
+ .join(", ")
250
+
251
+ const line = (label: string, m: Metrics): string => {
252
+ const { slices, cost } = m
253
+ const percent = m.refactorFirstRate === null ? "-" : `${Math.round(m.refactorFirstRate * 100)}%`
254
+ const continued = sum(m.continuations)
255
+ return [
256
+ `${label}: ${slices.decided} slices, ${slices.rebuilt} rebuilt (refactor-first ${percent}), ${slices.refactored} refactored`,
257
+ `${continued} continuations${continued > 0 ? ` (${listed(m.continuations)})` : ""}`,
258
+ `cost ${cost.total} (${listed(cost.byPhase)}; by model ${listed(cost.byModel) || "-"}), double work ${cost.doubleWork.total}`,
259
+ `${m.findings.total} findings${m.findings.total > 0 ? ` (${listed(m.findings.byLens)})` : ""}`,
260
+ ].join("; ")
261
+ }
262
+
263
+ export const runHistoryMetricsCommand = (
264
+ refs: readonly string[],
265
+ json: JsonMode,
266
+ out: ArtifactOut,
267
+ ): Effect.Effect<void, Error, CommandRequirements> =>
268
+ Effect.gen(function* () {
269
+ const entries = yield* historyEntries
270
+ const chosen =
271
+ refs.length === 0
272
+ ? entries
273
+ : (yield* Effect.forEach(refs, (ref) =>
274
+ // A branch name means every process kept from it, not just its newest.
275
+ resolveHistoryRef(ref, entries).pipe(
276
+ Effect.map((hit) =>
277
+ hit.branch === ref ? entries.filter((e) => e.branch === ref) : [hit],
278
+ ),
279
+ ),
280
+ )).flat()
281
+ const unique = chosen.filter((e, i) => chosen.findIndex((o) => o.ref === e.ref) === i)
282
+ const processes = yield* Effect.forEach(
283
+ unique,
284
+ (e) =>
285
+ readProcessMetrics(e).pipe(
286
+ Effect.map((metrics) => ({
287
+ ref: e.ref,
288
+ entry: e.entry,
289
+ branch: e.branch,
290
+ tip: e.tip,
291
+ ...metrics,
292
+ })),
293
+ ),
294
+ { concurrency: 4 },
295
+ )
296
+ const document = { processes, total: totalMetrics(processes) }
297
+ if (yield* writeJson(json, out, document)) return
298
+ if (processes.length === 0) {
299
+ out.write("no history refs; run gtd history fetch | sh to pull them\n")
300
+ return
301
+ }
302
+ const lines = processes.map((p) =>
303
+ line(
304
+ `${p.entry.slice(0, 7)} ${printable(p.branch ?? "-")}${p.workflow === null ? "" : ` (${printable(p.workflow)})`}`,
305
+ p,
306
+ ),
307
+ )
308
+ lines.push(line(`total (${processes.length} processes)`, document.total))
309
+ out.write(`${lines.join("\n")}\n`)
310
+ })
package/src/cli/Output.ts CHANGED
@@ -68,3 +68,16 @@ export type CommandRequirements =
68
68
  | ToolCommands
69
69
 
70
70
  export type SuccessExit = typeof EXIT_OK | typeof EXIT_STRAYS_FOUND
71
+
72
+ /** Writes `document` per `json` (bare → whole, `=path` → one value); returns false for `off`. */
73
+ export const writeJson = (
74
+ json: JsonMode,
75
+ out: ArtifactOut,
76
+ document: unknown,
77
+ ): Effect.Effect<boolean, Error> =>
78
+ Effect.gen(function* () {
79
+ if (json.kind === "off") return false
80
+ if (json.kind === "document") out.write(`${JSON.stringify(document)}\n`)
81
+ else yield* writeSelection(out, document, json.path)
82
+ return true
83
+ })
@@ -1694,6 +1694,7 @@ describe("runCommand — refuses in a repository with no commits", () => {
1694
1694
  judgeRun: { kind: "judgeRun", provider: "fixed" },
1695
1695
  historyList: { kind: "historyList" },
1696
1696
  historyShow: { kind: "historyShow", ref: "x" },
1697
+ historyMetrics: { kind: "historyMetrics", refs: [] },
1697
1698
  historyFetch: { kind: "historyFetch", remote: "origin" },
1698
1699
  strays: { kind: "strays", base: "HEAD", tip: "HEAD", globs: [] },
1699
1700
  }
@@ -1705,7 +1706,7 @@ describe("runCommand — refuses in a repository with no commits", () => {
1705
1706
  const NO_COMMITS_MESSAGE =
1706
1707
  "gtd requires a repository with at least one commit — make an initial commit, then run gtd again"
1707
1708
 
1708
- it("derives exactly the seventeen non-standalone kinds — a canary for the table-driven cases below", () => {
1709
+ it("derives exactly the eighteen non-standalone kinds — a canary for the table-driven cases below", () => {
1709
1710
  expect(stateKinds.sort()).toEqual(
1710
1711
  [
1711
1712
  "abandon",
@@ -1724,6 +1725,7 @@ describe("runCommand — refuses in a repository with no commits", () => {
1724
1725
  "judgeAnswer",
1725
1726
  "historyList",
1726
1727
  "historyShow",
1728
+ "historyMetrics",
1727
1729
  "historyFetch",
1728
1730
  ].sort(),
1729
1731
  )
@@ -109,6 +109,7 @@ export {
109
109
  } from "./Output.js"
110
110
  import { runStraysCommand } from "./Strays.js"
111
111
  import { runHistoryFetchCommand, runHistoryListCommand, runHistoryShowCommand } from "./History.js"
112
+ import { runHistoryMetricsCommand } from "./Metrics.js"
112
113
 
113
114
  /**
114
115
  * `Edge.ts`'s `Rest` is module-private (`.gtd/packages/05-step-core.md`
@@ -174,6 +175,7 @@ export type Command =
174
175
  | { readonly kind: "judgeAnswer" }
175
176
  | { readonly kind: "historyList" }
176
177
  | { readonly kind: "historyShow"; readonly ref: string }
178
+ | { readonly kind: "historyMetrics"; readonly refs: readonly string[] }
177
179
  | { readonly kind: "historyFetch"; readonly remote: string }
178
180
  | {
179
181
  readonly kind: "strays"
@@ -1320,6 +1322,8 @@ const dispatchVoidCommand = (
1320
1322
  return runHistoryListCommand(json, out)
1321
1323
  case "historyShow":
1322
1324
  return runHistoryShowCommand(command.ref, json, out)
1325
+ case "historyMetrics":
1326
+ return runHistoryMetricsCommand(command.refs, json, out)
1323
1327
  case "historyFetch":
1324
1328
  return runHistoryFetchCommand(command.remote, out)
1325
1329
  }
@@ -162,6 +162,10 @@ Given(
162
162
  },
163
163
  )
164
164
 
165
+ Given("step {string} deletes {string}", (world: TestkitWorld, step: string, path: string) => {
166
+ world.act(step, (_, repo) => repo.remove(path))
167
+ })
168
+
165
169
  // A standing assertion like any queued action: it applies on every later visit
166
170
  // to the step until `from now on` clears the list.
167
171
  Given(
@@ -156,7 +156,7 @@ export const contextHook = (input: {
156
156
  if (tokens >= limit.soft * window) {
157
157
  return {
158
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.`,
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
160
  }
161
161
  }
162
162
  return undefined
@@ -0,0 +1,65 @@
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
+ ## Acceptance Criteria
30
+
31
+ - AC-1: `subtract(5, 3)` returns 2.
32
+ - AC-2: `multiply(2, 3)` returns 6.
33
+ """
34
+ And step "design.gate.answer" changes nothing
35
+ And step "seam.check" changes nothing
36
+
37
+ Scenario: past the continuation cap the naive pass rests for a person, and landing there grants a new round
38
+ Given step "seam.naive" writes ".gtd/HANDOFF.md" with:
39
+ """
40
+ Roughed out subtract.
41
+ """
42
+ And step "seam.continue.naive" writes "tests/calc.test.ts" with:
43
+ """
44
+ it("[AC-1] subtracts", () => {})
45
+ """
46
+ And step "seam.continue.naive" writes ".gtd/HANDOFF.md" with:
47
+ """
48
+ Subtract is tested; multiply was next.
49
+ """
50
+ When the process is driven
51
+ Then the process rests at "seam.oversized"
52
+ And the resting step's content contains "1 continuation ran and the last still stopped at its context limit"
53
+ And the resting step's content contains "still open: AC-2"
54
+
55
+ When gtd lands
56
+ Then the process rests at "seam.continue.naive"
57
+ And the resting step's content contains "Still open: AC-2."
58
+ And the commit subjects end with:
59
+ """
60
+ gtd(human): design.gate.answer → seam.naive
61
+ gtd(agent): seam.naive → seam.continue.naive
62
+ gtd(agent): seam.continue.naive → seam.oversized
63
+ gtd(human): seam.oversized → seam.continue.naive
64
+ """
65
+
@@ -0,0 +1,88 @@
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
+ ## Acceptance Criteria
35
+
36
+ - AC-1: `subtract(5, 3)` returns 2.
37
+ - AC-2: `multiply(2, 3)` returns 6.
38
+ """
39
+ And step "design.gate.answer" changes nothing
40
+ And step "seam.check" changes nothing
41
+
42
+ Scenario: a rebuild stopped at its context limit lands its snapshot and is continued on the branch
43
+ Given step "seam.naive" writes "src/ops.ts" with:
44
+ """
45
+ export const subtract = (a: number, b: number) => a - b
46
+ export const multiply = (a: number, b: number) => a * b
47
+ """
48
+ And step "seam.naive" writes "tests/calc.test.ts" with:
49
+ """
50
+ it("adds", () => {})
51
+ it("[AC-1] subtracts", () => {})
52
+ it("[AC-2] multiplies", () => {})
53
+ """
54
+ And judge step "seam.judge" answers:
55
+ """
56
+ [{"id":"shape","answer":"refactor-first","p":0.9}]
57
+ """
58
+ And step "implement.building" writes "tests/calc.test.ts" with:
59
+ """
60
+ it("adds", () => {})
61
+ it("[AC-1] subtracts", () => {})
62
+ """
63
+ And step "implement.building" writes ".gtd/HANDOFF.md" with:
64
+ """
65
+ Rebuilt subtract behind the flag; multiply was next.
66
+ """
67
+ When the process is driven
68
+ Then the process rests at "implement.continue.building"
69
+ And the resting step's content contains "Still open: AC-2."
70
+ And the resting step's content contains "Its work is already in the tree."
71
+ And the resting step's content contains "Delete `.gtd/HANDOFF.md` once you are done"
72
+ And "tests/calc.test.ts" contains "[AC-1] subtracts"
73
+
74
+ Given step "implement.continue.building" writes "tests/calc.test.ts" with:
75
+ """
76
+ it("adds", () => {})
77
+ it("[AC-1] subtracts", () => {})
78
+ it("[AC-2] multiplies", () => {})
79
+ """
80
+ And step "implement.continue.building" deletes ".gtd/HANDOFF.md"
81
+ When the process is driven
82
+ Then the process rests at "implement.health.check"
83
+ And the commit subjects end with:
84
+ """
85
+ gtd(agent): implement.building → implement.continue.building
86
+ gtd(agent): implement.continue.building → implement.health.check
87
+ """
88
+
@@ -0,0 +1,64 @@
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
+ ## Acceptance Criteria
34
+
35
+ - AC-1: subtract(a, b) returns a - b.
36
+ - AC-2: multiply(a, b) returns a * b.
37
+ - AC-3: divide(a, b) returns a / b.
38
+ """
39
+ And step "design.slicing" writes ".gtd/SLICES.md" with:
40
+ """
41
+ # Slices
42
+
43
+ ## Subtract and divide
44
+
45
+ - AC-1
46
+ - AC-3
47
+
48
+ ## Multiply
49
+
50
+ - AC-2
51
+ """
52
+ And step "seam.naive" writes "src/sub.test.ts" with:
53
+ """
54
+ it("[AC-1] subtracts", () => {})
55
+ """
56
+ And step "seam.naive" writes ".gtd/HANDOFF.md" with:
57
+ """
58
+ Subtract is tested; divide was next.
59
+ """
60
+ When the process is driven
61
+ Then the process rests at "seam.continue.naive"
62
+ And the resting step's content contains "This slice builds AC-1, AC-3 only"
63
+ And the resting step's content contains "Still open: AC-3."
64
+ And the resting step's content does not contain "AC-2"