@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +36 -3
- package/dist/chunks/{Schema-DO5ttJaI.mjs → Schema-C6fLlQzx.mjs} +1 -1
- package/dist/chunks/{reserved-sfMdy7v7.mjs → reserved-C3PgP7XB.mjs} +56 -12
- package/dist/chunks/{tail-Cgca5Gp-.mjs → tail-Bkxki2nK.mjs} +1 -1
- package/dist/chunks/{wire-CSsa3Wxd.mjs → wire-BJuZWR3C.mjs} +1 -1
- package/dist/gtd.bundle.mjs +292 -53
- package/dist/testkit.mjs +257 -21
- package/dist/wire.mjs +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +158 -42
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +230 -70
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +15 -3
- package/src/cli/Cli.ts +61 -28
- package/src/cli/History.ts +1 -19
- package/src/cli/Metrics.test.ts +224 -0
- package/src/cli/Metrics.ts +310 -0
- package/src/cli/Output.ts +13 -0
- package/src/cli/Program.test.ts +3 -1
- package/src/cli/Program.ts +4 -0
- package/src/testkit/steps.ts +4 -0
- package/src/wire/ContextLimit.ts +1 -1
- package/src/workflows/feature/continuation-cap.feature +65 -0
- package/src/workflows/feature/continuation-rebuild.feature +88 -0
- package/src/workflows/feature/continuation-slices.feature +64 -0
- package/src/workflows/feature/continuation.feature +205 -0
- package/src/workflows/feature/criteria.ts +11 -0
- package/src/workflows/feature/refactor.ts +64 -30
- package/src/workflows/feature/seam.ts +102 -30
- package/src/workflows/shared/continuation.ts +92 -0
- package/src/workflows/shared/options.ts +2 -0
- package/src/workflows/shared/steps.ts +1 -0
- package/src/workflows/shared/text.test.ts +6 -2
- package/src/workflows/shared/text.ts +104 -8
- 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
|
+
})
|
package/src/cli/Program.test.ts
CHANGED
|
@@ -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
|
|
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
|
)
|
package/src/cli/Program.ts
CHANGED
|
@@ -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
|
}
|
package/src/testkit/steps.ts
CHANGED
|
@@ -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(
|
package/src/wire/ContextLimit.ts
CHANGED
|
@@ -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"
|