@pmelab/gtd 52.1.0 → 54.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 (94) 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-CXzUXJHL.mjs} +12 -5
  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 +321 -677
  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/testkit/steps.ts +23 -0
  39. package/src/wire/BeatDocument.ts +2 -12
  40. package/src/wire/BeatStatus.ts +0 -3
  41. package/src/wire/WireSchema.test.ts +0 -18
  42. package/src/wire/index.ts +0 -2
  43. package/src/wire/types.ts +0 -7
  44. package/src/workflow/Workflow.ts +1 -9
  45. package/src/workflows/feature/criteria.test.ts +1 -14
  46. package/src/workflows/feature/criteria.ts +6 -38
  47. package/src/workflows/feature/feature.ts +34 -118
  48. package/src/workflows/feature/hands-on-skip.feature +2 -70
  49. package/src/workflows/feature/index.ts +0 -1
  50. package/src/workflows/feature/lens-review.feature +50 -0
  51. package/src/workflows/feature/one-step-land.feature +0 -4
  52. package/src/workflows/feature/prototype-rounds.feature +1 -1
  53. package/src/workflows/feature/prototype.feature +3 -3
  54. package/src/workflows/feature/prototype.ts +18 -29
  55. package/src/workflows/feature/refactor-absorbs.feature +3 -8
  56. package/src/workflows/feature/refactor-first.feature +0 -5
  57. package/src/workflows/feature/refactor-rebuild.feature +0 -6
  58. package/src/workflows/feature/refactor-review.feature +49 -11
  59. package/src/workflows/feature/refactor.ts +30 -64
  60. package/src/workflows/feature/review-gate.feature +1 -1
  61. package/src/workflows/feature/reviewer-walkthrough.feature +0 -31
  62. package/src/workflows/feature/seam-rejection.feature +0 -4
  63. package/src/workflows/feature/seam.test.ts +7 -26
  64. package/src/workflows/feature/seam.ts +52 -165
  65. package/src/workflows/index.ts +0 -1
  66. package/src/workflows/shared/lenses.ts +0 -13
  67. package/src/workflows/shared/options.test.ts +2 -11
  68. package/src/workflows/shared/options.ts +4 -21
  69. package/src/workflows/shared/review.test.ts +3 -3
  70. package/src/workflows/shared/review.ts +12 -2
  71. package/src/workflows/shared/steps.ts +0 -2
  72. package/src/workflows/shared/text.test.ts +10 -80
  73. package/src/workflows/shared/text.ts +35 -211
  74. package/src/workflows/shared/vars.ts +2 -8
  75. package/src/workflows/shared/workflows.steps.ts +0 -13
  76. package/claude/hooks/context.ts +0 -66
  77. package/src/replay/Slices.test.ts +0 -73
  78. package/src/replay/Slices.ts +0 -45
  79. package/src/wire/ContextLimit.fixture.ts +0 -30
  80. package/src/wire/ContextLimit.test.ts +0 -96
  81. package/src/wire/ContextLimit.ts +0 -169
  82. package/src/wire/Slices.ts +0 -25
  83. package/src/wire/golden/1/slices.json +0 -11
  84. package/src/workflows/feature/context-limit.feature +0 -37
  85. package/src/workflows/feature/continuation-cap.feature +0 -69
  86. package/src/workflows/feature/continuation-rebuild.feature +0 -92
  87. package/src/workflows/feature/continuation-slices.feature +0 -68
  88. package/src/workflows/feature/continuation.feature +0 -209
  89. package/src/workflows/feature/slice-findings.feature +0 -256
  90. package/src/workflows/feature/slicing-cap.feature +0 -23
  91. package/src/workflows/feature/slicing.feature +0 -297
  92. package/src/workflows/feature/slicing.test.ts +0 -49
  93. package/src/workflows/feature/slicing.ts +0 -105
  94. package/src/workflows/shared/continuation.ts +0 -92
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "52.1.0",
3
+ "version": "54.0.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {
@@ -112,7 +112,6 @@ describe("parseArgv — scope", () => {
112
112
  ["judge", "--json"],
113
113
  ["judge", "answer", "--json"],
114
114
  ["doors", "--json"],
115
- ["slices", "--json"],
116
115
  ]) {
117
116
  const ok = parseArgv(["node", "gtd.js", ...args])
118
117
  expect(ok.kind).toBe("command")
@@ -929,79 +928,6 @@ describe("parseArgv — doors", () => {
929
928
  })
930
929
  })
931
930
 
932
- describe("parseArgv — slices", () => {
933
- const parse = (...args: string[]): CliPlan => parseArgv(["node", "gtd.js", ...args])
934
-
935
- it("`gtd slices` parses with every --json shape", () => {
936
- expect(parse("slices")).toMatchObject({
937
- kind: "command",
938
- command: { kind: "slices" },
939
- json: { kind: "off" },
940
- })
941
- expect(parse("slices", "--json")).toMatchObject({ json: { kind: "document" } })
942
- expect(parse("slices", "--json=slices.0.name")).toMatchObject({
943
- json: { kind: "select", path: "slices.0.name" },
944
- })
945
- })
946
-
947
- it("`gtd slices` takes no argument", () => {
948
- const plan = parse("slices", "extra")
949
- expect(plan.kind).toBe("usage")
950
- if (plan.kind === "usage") expect(plan.message).toContain("too many arguments")
951
- })
952
-
953
- it("help lists the command, and --json names it", () => {
954
- const help = renderHelp()
955
- expect(help).toMatch(/^ {2}slices\b/m)
956
- expect(help).toContain("gtd slices")
957
- })
958
-
959
- it("--json's scope error names gtd slices", () => {
960
- const plan = parse("lsp", "--json")
961
- expect(plan.kind).toBe("usage")
962
- if (plan.kind === "usage") expect(plan.message).toContain("`gtd slices`")
963
- })
964
- })
965
-
966
- describe("parseArgv — summary --slice", () => {
967
- const parse = (...args: string[]): CliPlan => parseArgv(["node", "gtd.js", ...args])
968
- const usage = (plan: CliPlan): string => (plan.kind === "usage" ? plan.message : "")
969
-
970
- it("`--slice build` and `--slice=build` parse to the slice name", () => {
971
- for (const plan of [parse("summary", "--slice", "build"), parse("summary", "--slice=build")]) {
972
- expect(plan).toMatchObject({ kind: "command", command: { kind: "summary", slice: "build" } })
973
- }
974
- })
975
-
976
- it("plain `summary` carries no slice key", () => {
977
- const plan = parse("summary")
978
- expect(plan.kind === "command" && plan.command).toEqual({ kind: "summary" })
979
- })
980
-
981
- it("bare `--slice` is a usage error naming the value form", () => {
982
- const plan = parse("summary", "--slice")
983
- expect(plan.kind).toBe("usage")
984
- expect(usage(plan)).toContain("--slice")
985
- expect(usage(plan)).toContain("value")
986
- })
987
-
988
- it("`--slice` twice is a usage error", () => {
989
- expect(parse("summary", "--slice", "a", "--slice", "b").kind).toBe("usage")
990
- })
991
-
992
- it("`--slice` on another command fails the scope check naming gtd summary", () => {
993
- for (const command of ["slices", "next"]) {
994
- const plan = parse(command, "--slice", "build")
995
- expect(plan.kind).toBe("usage")
996
- expect(usage(plan)).toContain("`gtd summary`")
997
- }
998
- })
999
-
1000
- it("help lists the flag", () => {
1001
- expect(renderHelp()).toContain("--slice=<name>")
1002
- })
1003
- })
1004
-
1005
931
  describe("parseArgv — gtd history", () => {
1006
932
  const parse = (...args: string[]) => parseArgv(["node", "gtd.js", ...args])
1007
933
  const usage = (plan: CliPlan): string => (plan.kind === "usage" ? plan.message : "")
@@ -1142,15 +1068,7 @@ describe("parseArgv — gtd strays", () => {
1142
1068
 
1143
1069
  describe("standaloneKinds / needsOf", () => {
1144
1070
  it("pins the standalone kinds", () => {
1145
- expect(standaloneKinds()).toEqual([
1146
- "lsp",
1147
- "check",
1148
- "uncheck",
1149
- "install",
1150
- "contextHook",
1151
- "judgeRun",
1152
- "strays",
1153
- ])
1071
+ expect(standaloneKinds()).toEqual(["lsp", "check", "uncheck", "install", "judgeRun", "strays"])
1154
1072
  })
1155
1073
 
1156
1074
  it("needsOf matches none/fs for the standalone kinds and state for everything else", () => {
@@ -1158,7 +1076,6 @@ describe("standaloneKinds / needsOf", () => {
1158
1076
  expect(needsOf("check")).toBe("fs")
1159
1077
  expect(needsOf("uncheck")).toBe("fs")
1160
1078
  expect(needsOf("install")).toBe("none")
1161
- expect(needsOf("contextHook")).toBe("none")
1162
1079
  expect(needsOf("judgeRun")).toBe("none")
1163
1080
  expect(needsOf("strays")).toBe("none")
1164
1081
  for (const kind of [
@@ -1173,7 +1090,6 @@ describe("standaloneKinds / needsOf", () => {
1173
1090
  "judgeAnswer",
1174
1091
  "door",
1175
1092
  "doors",
1176
- "slices",
1177
1093
  "historyList",
1178
1094
  "historyShow",
1179
1095
  "historyMetrics",
@@ -1251,7 +1167,6 @@ describe("renderHelp", () => {
1251
1167
  ),
1252
1168
  )
1253
1169
  expect(commands).toContain("judge answer")
1254
- expect(commands).toContain("context-hook")
1255
1170
  const listed = (file: string, list: RegExp): Set<string> => {
1256
1171
  const doc = readFileSync(resolve(import.meta.dirname, file), "utf8")
1257
1172
  const match = doc.match(list)
package/src/cli/Cli.ts CHANGED
@@ -36,7 +36,7 @@ import {
36
36
  } from "./Program.js"
37
37
  import type { ToolCommands } from "./ToolCommands.js"
38
38
  import { THRESHOLDS } from "./Judges.js"
39
- import { EXIT_RUNTIME_ERROR, EXIT_USAGE_ERROR, parseContextWindow } from "../wire/index.js"
39
+ import { EXIT_RUNTIME_ERROR, EXIT_USAGE_ERROR } from "../wire/index.js"
40
40
 
41
41
  export type { Command, CommandRequirements } from "./Program.js"
42
42
  export { needsOf, standaloneKinds } from "./Program.js"
@@ -91,20 +91,12 @@ const nonNegativeNumber = (raw: string, flag: string): Either.Either<number, str
91
91
  return Either.right(n)
92
92
  }
93
93
 
94
- const share = (raw: string, flag: string): Either.Either<number, string> => {
95
- const n = Number(raw)
96
- return raw.trim() !== "" && n > 0 && n <= 1
97
- ? Either.right(n)
98
- : Either.left(`gtd: ${flag} must be a share of the context window in (0, 1] — got "${raw}"`)
99
- }
100
-
101
94
  const JSON_KINDS: ReadonlySet<Command["kind"] | undefined> = new Set<Command["kind"]>([
102
95
  "next",
103
96
  "land",
104
97
  "judge",
105
98
  "judgeAnswer",
106
99
  "doors",
107
- "slices",
108
100
  "historyList",
109
101
  "historyShow",
110
102
  "historyMetrics",
@@ -120,11 +112,11 @@ const FLAGS: readonly FlagRow[] = [
120
112
  scope: (kind) => JSON_KINDS.has(kind),
121
113
  decode: ([raw]) => Either.right(raw ?? ""),
122
114
  scopeError:
123
- "gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer`/`gtd doors`/`gtd slices`/`gtd history list`/`gtd history show`/`gtd history metrics`/`gtd history judges`/`gtd strays` — " +
115
+ "gtd: --json is only valid for `gtd next`/`gtd land`/`gtd judge`/`gtd judge answer`/`gtd doors`/`gtd history list`/`gtd history show`/`gtd history metrics`/`gtd history judges`/`gtd strays` — " +
124
116
  "every other command prints plain text; see `gtd install` for the driver protocol briefing",
125
117
  valueHint: "<path>",
126
118
  help: [
127
- "(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd slices/gtd history",
119
+ "(gtd next/gtd land/gtd judge/gtd judge answer/gtd doors/gtd history",
128
120
  "list/gtd history show/gtd history metrics/gtd history judges/gtd strays only) output structured JSON. Bare --json prints the whole document;",
129
121
  "--json=<path> (a dotted key path into that document, e.g.",
130
122
  "kind, content, session.id) prints just that value: a",
@@ -258,56 +250,6 @@ const FLAGS: readonly FlagRow[] = [
258
250
  "GTD_JUDGE_MODEL, then judge.json, then sonnet",
259
251
  ],
260
252
  },
261
- {
262
- name: "--soft",
263
- arity: 1,
264
- repeatable: false,
265
- scope: (kind) => kind === "contextHook",
266
- decode: ([raw]) => share(raw ?? "", "--soft"),
267
- scopeError: "gtd: --soft is only valid for `gtd context-hook`",
268
- valueHint: "<share>",
269
- help: ["(gtd context-hook only) the soft limit; default 0.6"],
270
- },
271
- {
272
- name: "--hard",
273
- arity: 1,
274
- repeatable: false,
275
- scope: (kind) => kind === "contextHook",
276
- decode: ([raw]) => share(raw ?? "", "--hard"),
277
- scopeError: "gtd: --hard is only valid for `gtd context-hook`",
278
- valueHint: "<share>",
279
- help: ["(gtd context-hook only) the hard limit; default 0.7"],
280
- },
281
- {
282
- name: "--window",
283
- arity: 1,
284
- repeatable: false,
285
- scope: (kind) => kind === "contextHook",
286
- decode: ([raw]) => {
287
- const window = parseContextWindow(raw)
288
- return window !== undefined
289
- ? Either.right(window)
290
- : Either.left(`gtd: --window must be a positive integer — got "${raw ?? ""}"`)
291
- },
292
- scopeError: "gtd: --window is only valid for `gtd context-hook`",
293
- valueHint: "<tokens>",
294
- help: ["(gtd context-hook only) the model's context window in", "tokens; default 200000"],
295
- },
296
- {
297
- name: "--slice",
298
- arity: 1,
299
- repeatable: false,
300
- scope: (kind) => kind === "summary",
301
- decode: ([raw]) => Either.right(raw!),
302
- scopeError: "gtd: --slice is only valid for `gtd summary`",
303
- valueHint: "<name>",
304
- help: [
305
- "(gtd summary only) the closing-message prompt for one",
306
- "slice, named as `gtd slices` lists it: its own range, human",
307
- "commits and cost. An unknown name is refused (exit 1) with",
308
- "every valid slice listed",
309
- ],
310
- },
311
253
  {
312
254
  name: "--workflow",
313
255
  arity: 1,
@@ -642,22 +584,6 @@ const COMMAND_ROWS: readonly CommandRow[] = [
642
584
  "else it writes nothing.",
643
585
  ],
644
586
  },
645
- {
646
- token: "context-hook",
647
- kind: "contextHook",
648
- arity: "none",
649
- details: [
650
- "A Claude Code PostToolUse and PreCompact hook that stops an",
651
- "agent turn at a share of its context window. Reads the",
652
- "hook's input on stdin and the context the transcript's",
653
- "latest assistant message used. Past --soft it blocks with a",
654
- "request to end the turn with a handoff note; past --hard,",
655
- "or at PreCompact, it stops the turn with a stopReason",
656
- 'starting "gtd: context limit". Prints nothing otherwise,',
657
- "and for a subagent's tool call. Always exits 0 on valid",
658
- "input; see docs/driver.md",
659
- ],
660
- },
661
587
  {
662
588
  token: "summary",
663
589
  kind: "summary",
@@ -671,15 +597,10 @@ const COMMAND_ROWS: readonly CommandRow[] = [
671
597
  "no state transition, no file, no session identity — the",
672
598
  "driver pipes the output to a cold agent and does what it",
673
599
  "wants with the result (a squash, an amend, a PR body).",
674
- "--slice <name> prints it for one slice instead: its own",
675
- "range, human commits and cost (see `gtd slices`); it also",
676
- "refuses an unknown slice name, and a HEAD-closing process",
677
- "whose config files differ from the working tree or whose",
678
- "replay diverged; an open last slice is provisional. Refuses",
679
- "(exit 1) when the workflow declares no summary: template,",
680
- "or when the resolved run has no commits to name — plain",
681
- "gtd summary is runnable any time before the next thing",
682
- "lands on the branch",
600
+ "Refuses (exit 1) when the workflow declares no summary:",
601
+ "template, or when the resolved run has no commits to name —",
602
+ "it is runnable any time before the next thing lands on the",
603
+ "branch",
683
604
  ],
684
605
  },
685
606
  {
@@ -722,25 +643,6 @@ const COMMAND_ROWS: readonly CommandRow[] = [
722
643
  "value off it. Writes nothing",
723
644
  ],
724
645
  },
725
- {
726
- token: "slices",
727
- kind: "slices",
728
- arity: "none",
729
- details: [
730
- "List the slices of the process HEAD closes or sits inside,",
731
- "first slice first: one `name base..tip` line each (full",
732
- "hashes; plain output carries no promise). --json prints",
733
- "{ schema: 1, slices: [{ name, base, tip, closed }] };",
734
- "--json=<path> reads one value off it. Writes nothing: no",
735
- "git, no state transition, no session identity. When HEAD",
736
- "closes a process, every slice is listed as closed; a gtd",
737
- "version mismatch only warns on stderr. Refuses (exit 1)",
738
- "when HEAD neither closes nor sits inside a process, or when",
739
- "today's config disagrees with history about the process:",
740
- "HEAD closes it and its config files differ from the working",
741
- "tree, or the replay diverged",
742
- ],
743
- },
744
646
  {
745
647
  token: "judge",
746
648
  kind: "judge",
@@ -836,11 +738,11 @@ const COMMAND_ROWS: readonly CommandRow[] = [
836
738
  kind: "historyMetrics",
837
739
  arity: { name: "ref", many: true },
838
740
  details: [
839
- "Report how the kept processes went: slices whose naive pass",
840
- "landed or was rebuilt (the refactor-first rate), continuations",
841
- "per phase, Gtd-Cost by model and phase with the double work of",
842
- "a rebuild, quality-lap findings per lens, and red checks that",
843
- "passed on their re-check — per process and in total. Each <ref>",
741
+ "Report how the kept processes went: builds whose naive pass",
742
+ "landed or was rebuilt (the refactor-first rate), Gtd-Cost by",
743
+ "model and phase with the double work of a rebuild, quality-lap",
744
+ "findings per lens, and red checks that passed on their",
745
+ "re-check — per process and in total. Each <ref>",
844
746
  "is read as by history show, but a branch name means every",
845
747
  "process kept from it; none means every ref.",
846
748
  "Reads commit subjects and trailers, never replays, so a",
@@ -1282,10 +1184,6 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
1282
1184
  readonly "--dev"?: boolean
1283
1185
  readonly "--provider"?: "fixed" | "jev" | "llm"
1284
1186
  readonly "--answers"?: string
1285
- readonly "--slice"?: string
1286
- readonly "--soft"?: number
1287
- readonly "--hard"?: number
1288
- readonly "--window"?: number
1289
1187
  readonly "--jsonl"?: boolean
1290
1188
  }
1291
1189
 
@@ -1390,27 +1288,10 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
1390
1288
  }
1391
1289
  }
1392
1290
 
1393
- if (kind === "contextHook") {
1394
- return {
1395
- kind: "command",
1396
- command: {
1397
- kind: "contextHook",
1398
- ...(bag["--soft"] !== undefined ? { soft: bag["--soft"] } : {}),
1399
- ...(bag["--hard"] !== undefined ? { hard: bag["--hard"] } : {}),
1400
- ...(bag["--window"] !== undefined ? { window: bag["--window"] } : {}),
1401
- },
1402
- json,
1403
- verbose,
1404
- }
1405
- }
1406
-
1407
1291
  if (kind === "summary") {
1408
1292
  return {
1409
1293
  kind: "command",
1410
- command: {
1411
- kind: "summary",
1412
- ...(bag["--slice"] !== undefined ? { slice: bag["--slice"] } : {}),
1413
- },
1294
+ command: { kind: "summary" },
1414
1295
  json,
1415
1296
  verbose,
1416
1297
  }
@@ -1515,7 +1396,6 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
1515
1396
  | "validate"
1516
1397
  | "install"
1517
1398
  | "doors"
1518
- | "slices"
1519
1399
  | "base"
1520
1400
  | "judge"
1521
1401
  | "judgeAnswer"
@@ -10,50 +10,50 @@ const turn = (subject: string, opts: { cost?: string; findings?: number } = {}):
10
10
  })
11
11
 
12
12
  describe("processMetrics", () => {
13
- it("counts a naive pass applied as it is as a landed slice", () => {
13
+ it("counts a naive pass applied as it is as a landed build", () => {
14
14
  const m = processMetrics([
15
15
  turn("gtd(human): idle → seam.naive"),
16
16
  turn("gtd(agent): seam.naive → seam.check", { cost: "0.4 coder" }),
17
17
  turn("gtd(check): seam.check → implement.applying"),
18
18
  turn("gtd(check): implement.applying → idle"),
19
19
  ])
20
- expect(m.slices).toEqual({ decided: 1, landed: 1, rebuilt: 0, refactored: 0 })
20
+ expect(m.builds).toEqual({ decided: 1, landed: 1, rebuilt: 0, refactored: 0 })
21
21
  expect(m.refactorFirstRate).toBe(0)
22
22
  expect(m.cost.byPhase).toEqual({ naive: 0.4, refactor: 0, implement: 0, other: 0 })
23
23
  expect(m.cost.doubleWork).toEqual({ naive: 0, rebuild: 0, total: 0 })
24
24
  })
25
25
 
26
- it("charges a rebuilt slice's naive pass and rebuild as double work, refactor apart", () => {
26
+ it("charges a rebuilt build's naive pass and rebuild as double work, refactor apart", () => {
27
27
  const m = processMetrics([
28
28
  turn("gtd(human): idle → seam.naive"),
29
- turn("gtd(agent): seam.naive → seam.continue.naive", { cost: "0.4 coder" }),
30
- turn("gtd(agent): seam.continue.naive → seam.judge", { cost: "0.2 coder" }),
29
+ turn("gtd(agent): seam.naive → seam.check", { cost: "0.4 coder" }),
30
+ turn("gtd(check): seam.check → seam.naive"),
31
+ turn("gtd(agent): seam.naive → seam.judge", { cost: "0.2 coder" }),
31
32
  turn("gtd(judge): seam.judge → seam.note", { cost: "0.01 judge" }),
32
33
  turn("gtd(agent): seam.note → refactor.deriving", { cost: "0.05 coder" }),
33
34
  turn("gtd(check): refactor.deriving → refactor.building"),
34
35
  turn("gtd(agent): refactor.building → refactor.closing", { cost: "0.3 coder" }),
35
36
  turn("gtd(check): refactor.closing → implement.building"),
36
- turn("gtd(agent): implement.building → implement.continue.building", { cost: "0.5 coder" }),
37
- turn("gtd(agent): implement.continue.building → idle", { cost: "0.25 coder" }),
37
+ turn("gtd(agent): implement.building → implement.health.check", { cost: "0.5 coder" }),
38
+ turn("gtd(agent): implement.fix → idle", { cost: "0.25 coder" }),
38
39
  ])
39
- expect(m.slices).toEqual({ decided: 1, landed: 0, rebuilt: 1, refactored: 1 })
40
+ expect(m.builds).toEqual({ decided: 1, landed: 0, rebuilt: 1, refactored: 1 })
40
41
  expect(m.refactorFirstRate).toBe(1)
41
- expect(m.continuations).toEqual({ "seam.naive": 1, "implement.building": 1 })
42
42
  expect(m.cost.total).toBe(1.71)
43
43
  expect(m.cost.byModel).toEqual({ coder: 1.7, judge: 0.01 })
44
44
  expect(m.cost.byPhase).toEqual({ naive: 0.6, refactor: 0.35, implement: 0.75, other: 0.01 })
45
45
  expect(m.cost.doubleWork).toEqual({ naive: 0.6, rebuild: 0.75, total: 1.35 })
46
46
  })
47
47
 
48
- it("rebuilds without a refactor when the slice leaves the seam for a build turn", () => {
48
+ it("rebuilds without a refactor when the build leaves the seam for a build turn", () => {
49
49
  const m = processMetrics([
50
50
  turn("gtd(judge): seam.judge → implement.building"),
51
51
  turn("gtd(agent): implement.building → idle"),
52
52
  ])
53
- expect(m.slices).toEqual({ decided: 1, landed: 0, rebuilt: 1, refactored: 0 })
53
+ expect(m.builds).toEqual({ decided: 1, landed: 0, rebuilt: 1, refactored: 0 })
54
54
  })
55
55
 
56
- it("keeps each slice's decision apart", () => {
56
+ it("keeps each build's decision apart", () => {
57
57
  const m = processMetrics([
58
58
  turn("gtd(agent): seam.naive → seam.check", { cost: "1" }),
59
59
  turn("gtd(check): seam.check → implement.applying"),
@@ -61,20 +61,19 @@ describe("processMetrics", () => {
61
61
  turn("gtd(check): implement.closing → seam.naive"),
62
62
  turn("gtd(agent): seam.naive → seam.note", { cost: "2" }),
63
63
  turn("gtd(agent): seam.note → refactor.deriving"),
64
- turn("gtd(agent): refactor.continue.building → refactor.closing", { cost: "1" }),
64
+ turn("gtd(agent): refactor.building → refactor.closing", { cost: "1" }),
65
65
  turn("gtd(check): refactor.closing → implement.building"),
66
66
  turn("gtd(agent): implement.building → idle", { cost: "3" }),
67
67
  ])
68
- expect(m.slices).toEqual({ decided: 2, landed: 1, rebuilt: 1, refactored: 1 })
68
+ expect(m.builds).toEqual({ decided: 2, landed: 1, rebuilt: 1, refactored: 1 })
69
69
  expect(m.refactorFirstRate).toBe(0.5)
70
- expect(m.continuations).toEqual({ "refactor.building": 1 })
71
70
  expect(m.cost.byModel).toEqual({ unspecified: 8 })
72
71
  expect(m.cost.doubleWork).toEqual({ naive: 2, rebuild: 3, total: 5 })
73
72
  })
74
73
 
75
- it("counts a naive retry in a fresh conversation as naive cost", () => {
74
+ it("counts a naive pass under an older history's scope as naive cost", () => {
76
75
  const m = processMetrics([
77
- turn("gtd(agent): seam.continue.naive → seam.judge", { cost: "1" }),
76
+ turn("gtd(agent): seam.naive → seam.judge", { cost: "1" }),
78
77
  turn("gtd(judge): seam.judge → seam.fresh.naive"),
79
78
  turn("gtd(agent): seam.fresh.naive → seam.judge", { cost: "2" }),
80
79
  turn("gtd(judge): seam.judge → implement.building"),
@@ -83,14 +82,14 @@ describe("processMetrics", () => {
83
82
  expect(m.cost.doubleWork.naive).toBe(3)
84
83
  })
85
84
 
86
- it("turns a landed slice whose naive pass fails to apply into a rebuild", () => {
85
+ it("turns a landed build whose naive pass fails to apply into a rebuild", () => {
87
86
  const m = processMetrics([
88
87
  turn("gtd(agent): seam.naive → seam.check", { cost: "1" }),
89
88
  turn("gtd(check): seam.check → implement.applying"),
90
89
  turn("gtd(check): implement.applying → implement.building"),
91
90
  turn("gtd(agent): implement.building → implement.health.check", { cost: "2" }),
92
91
  ])
93
- expect(m.slices).toEqual({ decided: 1, landed: 0, rebuilt: 1, refactored: 0 })
92
+ expect(m.builds).toEqual({ decided: 1, landed: 0, rebuilt: 1, refactored: 0 })
94
93
  expect(m.cost.doubleWork).toEqual({ naive: 1, rebuild: 2, total: 3 })
95
94
  })
96
95
 
@@ -107,7 +106,7 @@ describe("processMetrics", () => {
107
106
  })
108
107
  })
109
108
 
110
- it("ends a rebuild's double work at the slice's closing", () => {
109
+ it("ends a rebuild's double work at the build's closing", () => {
111
110
  const m = processMetrics([
112
111
  turn("gtd(judge): seam.judge → implement.building"),
113
112
  turn("gtd(agent): implement.building → implement.closing", { cost: "1" }),
@@ -134,14 +133,6 @@ describe("processMetrics", () => {
134
133
  expect(processMetrics([turn("gtd(human): idle → building")]).refactorFirstRate).toBeNull()
135
134
  })
136
135
 
137
- it("counts continuations only for agent turns, the bare form included", () => {
138
- const m = processMetrics([
139
- turn("gtd(agent): seam.continue.naive"),
140
- turn("gtd(human): seam.continue.naive → seam.oversized"),
141
- ])
142
- expect(m.continuations).toEqual({ "seam.naive": 1 })
143
- })
144
-
145
136
  it("sums findings per quality lens and skips commits gtd did not write", () => {
146
137
  const m = processMetrics([
147
138
  turn(
@@ -201,16 +192,15 @@ describe("totalMetrics", () => {
201
192
  turn("gtd(check): seam.check → implement.applying"),
202
193
  ])
203
194
  const b = processMetrics([
204
- turn("gtd(agent): seam.continue.naive → seam.note", { cost: "2 b" }),
195
+ turn("gtd(agent): seam.naive → seam.note", { cost: "2 b" }),
205
196
  turn("gtd(agent): seam.note → refactor.deriving"),
206
197
  turn("gtd(agent): seam.naive → seam.note"),
207
198
  turn("gtd(agent): seam.note → refactor.deriving"),
208
199
  ])
209
200
  const total = totalMetrics([a, b])
210
201
  expect(total.processes).toBe(2)
211
- expect(total.slices).toEqual({ decided: 3, landed: 1, rebuilt: 2, refactored: 2 })
202
+ expect(total.builds).toEqual({ decided: 3, landed: 1, rebuilt: 2, refactored: 2 })
212
203
  expect(total.refactorFirstRate).toBe(0.667)
213
- expect(total.continuations).toEqual({ "seam.naive": 1 })
214
204
  expect(total.cost.byModel).toEqual({ a: 1, b: 2 })
215
205
  })
216
206
  })