@pmelab/gtd 58.2.0 → 59.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +115 -390
- package/dist/chunks/{Schema-B3o6eRNU.mjs → testkit-CMBAAyRK.mjs} +13409 -3351
- package/dist/gtd.bundle.mjs +76 -59
- package/dist/testkit/gherkin.mjs +1 -1
- package/dist/testkit.mjs +1 -1
- package/package.json +3 -42
- package/skills/authoring/SKILL.md +31 -25
- package/src/cli/Cli.test.ts +15 -19
- package/src/cli/Cli.ts +2 -4
- package/src/cli/Program.test.ts +60 -29
- package/src/cli/Program.ts +5 -7
- package/src/edge/Edge.ts +3 -3
- package/src/flags/index.test.ts +0 -13
- package/src/flags/index.ts +1 -3
- package/src/install/Install.test.ts +14 -11
- package/src/install/Install.ts +38 -15
- package/src/install/index.ts +1 -2
- package/src/replay/Replay.test.ts +1 -1
- package/src/replay/index.ts +1 -1
- package/src/step/planStart.ts +6 -1
- package/src/twins/FlowScripts.ts +1 -1
- package/src/workflow/index.ts +1 -0
- package/src/workflow/load.test.ts +138 -113
- package/src/workflow/load.ts +1 -1
- package/src/workflow/vars.test.ts +30 -3
- package/src/workflow/vars.ts +34 -2
- package/src/workflow-kit/index.ts +1 -1
- package/dist/chunks/reserved-lMYCaapY.mjs +0 -1990
- package/dist/chunks/review-Bfaror38.mjs +0 -427
- package/dist/chunks/tail-Bd7L940p.mjs +0 -355
- package/dist/chunks/testkit-JDtpNKyi.mjs +0 -10410
- package/dist/workflows/cleanup.mjs +0 -36
- package/dist/workflows/feature.mjs +0 -1018
- package/dist/workflows/fix.mjs +0 -41
- package/dist/workflows/review.mjs +0 -45
- package/dist/workflows/rollout.mjs +0 -48
- package/dist/workflows.mjs +0 -40651
- package/src/install/ConfigScaffold.ts +0 -22
- package/src/workflows/cleanup/cleanup-path.feature +0 -94
- package/src/workflows/cleanup/cleanup.feature +0 -315
- package/src/workflows/cleanup/cleanup.test.ts +0 -276
- package/src/workflows/cleanup/cleanup.ts +0 -52
- package/src/workflows/cleanup/index.ts +0 -6
- package/src/workflows/feature/acceptance-criteria-tier.feature +0 -25
- package/src/workflows/feature/acceptance-criteria.feature +0 -389
- package/src/workflows/feature/criteria-coverage.feature +0 -102
- package/src/workflows/feature/criteria.test.ts +0 -147
- package/src/workflows/feature/criteria.ts +0 -267
- package/src/workflows/feature/fast-path.feature +0 -93
- package/src/workflows/feature/feature.test.ts +0 -338
- package/src/workflows/feature/feature.ts +0 -156
- package/src/workflows/feature/green.ts +0 -19
- package/src/workflows/feature/index.ts +0 -18
- package/src/workflows/feature/lens-models.feature +0 -69
- package/src/workflows/feature/lens-review.feature +0 -566
- package/src/workflows/feature/naive-acceptance.feature +0 -147
- package/src/workflows/feature/naive-test.feature +0 -263
- package/src/workflows/feature/one-step-land.feature +0 -93
- package/src/workflows/feature/planning.ts +0 -41
- package/src/workflows/feature/refactor-absorbs.feature +0 -131
- package/src/workflows/feature/refactor-first.feature +0 -158
- package/src/workflows/feature/refactor-lens-models.feature +0 -79
- package/src/workflows/feature/refactor-rebuild.feature +0 -273
- package/src/workflows/feature/refactor-review.feature +0 -1528
- package/src/workflows/feature/refactor.test.ts +0 -1220
- package/src/workflows/feature/refactor.ts +0 -368
- package/src/workflows/feature/release-flag.feature +0 -167
- package/src/workflows/feature/release-flag.test.ts +0 -49
- package/src/workflows/feature/release-flag.ts +0 -46
- package/src/workflows/feature/review-gate.feature +0 -241
- package/src/workflows/feature/reviewer-walkthrough.feature +0 -142
- package/src/workflows/feature/risk-tier.feature +0 -351
- package/src/workflows/feature/seam-rejection.feature +0 -146
- package/src/workflows/feature/seam.feature +0 -297
- package/src/workflows/feature/seam.test.ts +0 -527
- package/src/workflows/feature/seam.ts +0 -426
- package/src/workflows/feature/standards.feature +0 -354
- package/src/workflows/feature/steps.test.ts +0 -65
- package/src/workflows/feature/steps.ts +0 -45
- package/src/workflows/feature/tier-pin.feature +0 -66
- package/src/workflows/feature/tier.test.ts +0 -166
- package/src/workflows/feature/tier.ts +0 -56
- package/src/workflows/fix/fix-lens-origin.feature +0 -114
- package/src/workflows/fix/fix-quality.feature +0 -212
- package/src/workflows/fix/fix.feature +0 -79
- package/src/workflows/fix/fix.test.ts +0 -74
- package/src/workflows/fix/fix.ts +0 -52
- package/src/workflows/fix/index.ts +0 -6
- package/src/workflows/index.ts +0 -47
- package/src/workflows/review/index.ts +0 -6
- package/src/workflows/review/review-door.feature +0 -199
- package/src/workflows/review/review.test.ts +0 -27
- package/src/workflows/review/review.ts +0 -60
- package/src/workflows/rollout/index.ts +0 -6
- package/src/workflows/rollout/rollout.feature +0 -510
- package/src/workflows/rollout/rollout.test.ts +0 -263
- package/src/workflows/rollout/rollout.ts +0 -82
- package/src/workflows/shared/__fixtures__/feature-collecting-prompt.txt +0 -98
- package/src/workflows/shared/access.test.ts +0 -62
- package/src/workflows/shared/access.ts +0 -24
- package/src/workflows/shared/config.fixture.ts +0 -12
- package/src/workflows/shared/fences.ts +0 -24
- package/src/workflows/shared/health.test.ts +0 -148
- package/src/workflows/shared/health.ts +0 -191
- package/src/workflows/shared/learn.test.ts +0 -140
- package/src/workflows/shared/learn.ts +0 -201
- package/src/workflows/shared/lenses.test.ts +0 -39
- package/src/workflows/shared/lenses.ts +0 -294
- package/src/workflows/shared/options.test.ts +0 -157
- package/src/workflows/shared/options.ts +0 -92
- package/src/workflows/shared/progress.feature +0 -124
- package/src/workflows/shared/prose.ts +0 -263
- package/src/workflows/shared/recheck.feature +0 -60
- package/src/workflows/shared/registry.test.ts +0 -345
- package/src/workflows/shared/registry.ts +0 -183
- package/src/workflows/shared/reserved.ts +0 -11
- package/src/workflows/shared/review.test.ts +0 -1062
- package/src/workflows/shared/review.ts +0 -446
- package/src/workflows/shared/skills.test.ts +0 -121
- package/src/workflows/shared/skills.ts +0 -49
- package/src/workflows/shared/standards.test.ts +0 -90
- package/src/workflows/shared/standards.ts +0 -156
- package/src/workflows/shared/steps.test.ts +0 -219
- package/src/workflows/shared/steps.ts +0 -201
- package/src/workflows/shared/tail.test.ts +0 -454
- package/src/workflows/shared/tail.ts +0 -264
- package/src/workflows/shared/text.fixture.ts +0 -128
- package/src/workflows/shared/text.test.ts +0 -1430
- package/src/workflows/shared/text.ts +0 -1700
- package/src/workflows/shared/threads.test.ts +0 -129
- package/src/workflows/shared/vars.ts +0 -47
- package/src/workflows/shared/workflows.steps.ts +0 -54
|
@@ -1,426 +0,0 @@
|
|
|
1
|
-
import { posix } from "node:path"
|
|
2
|
-
import {
|
|
3
|
-
agent,
|
|
4
|
-
applyScript,
|
|
5
|
-
changes,
|
|
6
|
-
changesSince,
|
|
7
|
-
check,
|
|
8
|
-
codeChanges,
|
|
9
|
-
env,
|
|
10
|
-
head,
|
|
11
|
-
human,
|
|
12
|
-
judgeDiffShape,
|
|
13
|
-
kept,
|
|
14
|
-
quote,
|
|
15
|
-
read,
|
|
16
|
-
refuse,
|
|
17
|
-
removeScript,
|
|
18
|
-
restoreScript,
|
|
19
|
-
run,
|
|
20
|
-
scope,
|
|
21
|
-
vars,
|
|
22
|
-
wrote,
|
|
23
|
-
type Change,
|
|
24
|
-
type DiffShapeResult,
|
|
25
|
-
type KeptTip,
|
|
26
|
-
} from "../../flows/index.js"
|
|
27
|
-
import { coder, FEEDBACK, NAIVE_TEST, REFACTOR_NOTE, REQUIREMENTS } from "../shared/steps.js"
|
|
28
|
-
import * as t from "../shared/text.js"
|
|
29
|
-
import { defaults } from "../shared/vars.js"
|
|
30
|
-
import { green } from "./green.js"
|
|
31
|
-
import { criterionIds, requireCriteria, requireCriteriaFrozen } from "./criteria.js"
|
|
32
|
-
import {
|
|
33
|
-
COMMENT,
|
|
34
|
-
holdsCallSites,
|
|
35
|
-
isTestFile,
|
|
36
|
-
refactorStrays,
|
|
37
|
-
reviewedRefactor,
|
|
38
|
-
} from "./refactor.js"
|
|
39
|
-
import { isCode } from "../shared/review.js"
|
|
40
|
-
import { naiveTestOn } from "../shared/options.js"
|
|
41
|
-
import { flagged, requestedFlag } from "./release-flag.js"
|
|
42
|
-
import type { RiskTier } from "./tier.js"
|
|
43
|
-
|
|
44
|
-
/** Naive-pass retries after a `reject` or a red suite; the next one rests at `seam.steer`. */
|
|
45
|
-
const NAIVE_RETRIES = 2
|
|
46
|
-
|
|
47
|
-
/** A change to an existing file outside the allowlist is a stray even when the task needs it: the judge decides. */
|
|
48
|
-
export const judgeSeam = (tip: KeptTip): Promise<DiffShapeResult> =>
|
|
49
|
-
judgeDiffShape("judge", {
|
|
50
|
-
changes: tip.changes,
|
|
51
|
-
allow: (vars.seamAllow ?? defaults.seamAllow!)
|
|
52
|
-
.split(",")
|
|
53
|
-
.map((glob) => glob.trim())
|
|
54
|
-
.filter((glob) => glob !== ""),
|
|
55
|
-
exempt: tip.changes.filter((c) => c.status === "added").map((c) => c.path),
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
export const naivePass = (retry?: t.NaiveRetry | t.NaiveTested): Promise<void> =>
|
|
59
|
-
t.agentWithSkills("naive", t.naivePassPrompt(retry), {
|
|
60
|
-
label: retry === undefined ? "Writing the naive pass" : "Rewriting the naive pass",
|
|
61
|
-
keep: true,
|
|
62
|
-
model: coder(),
|
|
63
|
-
system: t.builderSystem(),
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
// Only a string an import, `require` or `mock` call takes: a URL route or path-like data is not one.
|
|
67
|
-
const IMPORTED = /(?:\bfrom|\bimport|\brequire\s*\(|\bmock\s*\()\s*\(?\s*(["'`])([^"'`\n]+)\1/g
|
|
68
|
-
const EXTENSION = /\.[\w-]+$/
|
|
69
|
-
const ALIAS = /^(?:[@~]\/|#)/
|
|
70
|
-
|
|
71
|
-
/** The specifiers an added `path` answers to: itself, without its extension, and its directory for an `index`. */
|
|
72
|
-
const namesOf = (path: string): string[] => {
|
|
73
|
-
const bare = path.replace(EXTENSION, "")
|
|
74
|
-
return posix.basename(bare) === "index" ? [path, bare, posix.dirname(bare)] : [path, bare]
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* The repository paths `test` imports, each with and without an extension: a
|
|
79
|
-
* relative specifier resolved from its directory, an aliased one (`@/`, `~/`,
|
|
80
|
-
* `#`) as a path tail, anything else as written. Comments are skipped; a
|
|
81
|
-
* computed import goes unseen.
|
|
82
|
-
*/
|
|
83
|
-
const importsOf = (test: Change): { readonly path: string; readonly tail: boolean }[] =>
|
|
84
|
-
[...(test.after ?? "").replace(COMMENT, "").matchAll(IMPORTED)].flatMap(([, , spec]) => {
|
|
85
|
-
const relative = /^\.\.?(\/|$)/.test(spec!)
|
|
86
|
-
const tail = ALIAS.test(spec!)
|
|
87
|
-
const path = relative ? posix.join(posix.dirname(test.path), spec!) : spec!.replace(ALIAS, "")
|
|
88
|
-
return [path, path.replace(EXTENSION, "")].map((p) => ({ path: p, tail }))
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
const reaches = (imports: ReturnType<typeof importsOf>, added: string): boolean =>
|
|
92
|
-
namesOf(added).some((name) =>
|
|
93
|
-
imports.some(({ path, tail }) => name === path || (tail && name.endsWith(`/${path}`))),
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
const acceptanceTests = (changes: readonly Change[]): readonly Change[] =>
|
|
97
|
-
changes.filter((c) => c.after !== undefined && isCode(c.path) && isTestFile(c.path))
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Why `tip`'s tests fall short, or `undefined`: a test importing a file the
|
|
101
|
-
* pass added — naive structure a reset and refactor replaces, which the tests
|
|
102
|
-
* must survive.
|
|
103
|
-
*/
|
|
104
|
-
export const acceptanceFault = (tip: KeptTip): string | undefined => {
|
|
105
|
-
const added = tip.changes
|
|
106
|
-
.filter((c) => c.status === "added" && isCode(c.path) && !isTestFile(c.path))
|
|
107
|
-
.map((c) => c.path)
|
|
108
|
-
const reaching = acceptanceTests(tip.changes)
|
|
109
|
-
.filter((test) => holdsCallSites(test.path))
|
|
110
|
-
.flatMap((test) => {
|
|
111
|
-
const imports = importsOf(test)
|
|
112
|
-
return added
|
|
113
|
-
.filter((path) => reaches(imports, path))
|
|
114
|
-
.map((path) => ` - ${test.path} → ${path}`)
|
|
115
|
-
})
|
|
116
|
-
if (reaching.length > 0)
|
|
117
|
-
return `acceptance-internals: these tests import files the naive pass added — test through code that existed before it (a public entry point, an existing module's exports):\n${reaching.join("\n")}`
|
|
118
|
-
return undefined
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** What a rebuild carries over from the naive pass `tip`: every test file it added or changed. */
|
|
122
|
-
export const naiveAcceptance = (tip: KeptTip): t.Acceptance => ({
|
|
123
|
-
hash: tip.hash,
|
|
124
|
-
tests: acceptanceTests(tip.changes).map((c) => c.path),
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
const restore = (path: string): string =>
|
|
128
|
-
`if git cat-file -e ${quote(`HEAD:${path}`)} 2>/dev/null; then git checkout -q HEAD -- ${quote(path)}; else rm -f -- ${quote(path)}; fi`
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Run the suite with `tip`'s diff applied, then take it out again; resolves
|
|
132
|
-
* `true` when green. The pass's paths go back to HEAD before (an interrupted
|
|
133
|
-
* run left them applied) and after; afterwards so does anything else the run
|
|
134
|
-
* changed, judged against what was already dirty, which is left alone.
|
|
135
|
-
*/
|
|
136
|
-
const naiveGreen = (tip: KeptTip): Promise<boolean> => {
|
|
137
|
-
const paths = tip.changes.map((c) => restore(c.path))
|
|
138
|
-
return check(
|
|
139
|
-
"check",
|
|
140
|
-
[
|
|
141
|
-
...paths,
|
|
142
|
-
"dirty=$(git -c core.quotePath=false ls-files -m -d -o --exclude-standard)",
|
|
143
|
-
`if ! git diff --binary --no-color --no-ext-diff --no-textconv --src-prefix=a/ --dst-prefix=b/ ${quote(`${tip.hash}^`)} ${quote(tip.hash)} -- | git apply --whitespace=nowarn; then`,
|
|
144
|
-
" echo 'gtd: the kept naive pass no longer applies to HEAD; no test ran'",
|
|
145
|
-
" exit 1",
|
|
146
|
-
"fi",
|
|
147
|
-
"(",
|
|
148
|
-
env.testCommand ?? "",
|
|
149
|
-
")",
|
|
150
|
-
"code=$?",
|
|
151
|
-
...paths,
|
|
152
|
-
"git -c core.quotePath=false ls-files -m -d -o --exclude-standard | while IFS= read -r path; do",
|
|
153
|
-
' printf \'%s\\n\' "$dirty" | grep -qxF -- "$path" && continue',
|
|
154
|
-
' if git cat-file -e "HEAD:$path" 2>/dev/null; then git checkout -q HEAD -- "$path"; else rm -f -- "$path"; fi',
|
|
155
|
-
"done",
|
|
156
|
-
'exit "$code"',
|
|
157
|
-
].join("\n"),
|
|
158
|
-
{ report: FEEDBACK, label: "Running the naive pass's tests" },
|
|
159
|
-
)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export type { Acceptance, Rebuild } from "../shared/text.js"
|
|
163
|
-
|
|
164
|
-
export interface ImplementOptions {
|
|
165
|
-
readonly reference?: string | undefined
|
|
166
|
-
readonly tier?: RiskTier | undefined
|
|
167
|
-
readonly amend?: boolean | undefined
|
|
168
|
-
/** After a refactor, which may already carry the feature: an empty turn completes the step. */
|
|
169
|
-
readonly afterRefactor?: boolean | undefined
|
|
170
|
-
/** A kept naive tip to apply as the implementation; built fresh only when it no longer applies. */
|
|
171
|
-
readonly naive?: string | undefined
|
|
172
|
-
/** A rebuild after a refactor-first judgment: the note it plugs into, the acceptance tests it must carry. */
|
|
173
|
-
readonly rebuild?: t.Rebuild | undefined
|
|
174
|
-
/** @deprecated Ignored since the coverage judge is gone. */
|
|
175
|
-
readonly base?: string | undefined
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** Resolves `false` when the tip no longer applies (the failure is in `.gtd/FEEDBACK.md`) or has nothing outside `.gtd/` to apply. */
|
|
179
|
-
const applied = async (tip: string): Promise<boolean> => {
|
|
180
|
-
await run("applying", applyScript(tip, FEEDBACK, t.naiveApplyFailure(tip), [".gtd"]), {
|
|
181
|
-
label: "Applying the naive pass",
|
|
182
|
-
})
|
|
183
|
-
return !wrote(FEEDBACK) && codeChanges().length > 0
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const build = async (options: ImplementOptions): Promise<void> => {
|
|
187
|
-
await t.agentWithSkills(
|
|
188
|
-
"building",
|
|
189
|
-
t.implementBuildingPrompt(
|
|
190
|
-
options.reference,
|
|
191
|
-
options.amend,
|
|
192
|
-
options.afterRefactor,
|
|
193
|
-
requestedFlag(),
|
|
194
|
-
options.rebuild,
|
|
195
|
-
),
|
|
196
|
-
{
|
|
197
|
-
label: "Implementing",
|
|
198
|
-
model: coder(),
|
|
199
|
-
system: t.builderSystem(),
|
|
200
|
-
...(options.afterRefactor === true ? { allowEmpty: true } : {}),
|
|
201
|
-
},
|
|
202
|
-
)
|
|
203
|
-
requireCriteriaFrozen(REQUIREMENTS)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export function implement(options?: ImplementOptions): Promise<void>
|
|
207
|
-
/** @deprecated Pass `implement({ reference, tier, amend })`. */
|
|
208
|
-
export function implement(
|
|
209
|
-
reference: string | undefined,
|
|
210
|
-
tier?: RiskTier,
|
|
211
|
-
amend?: boolean,
|
|
212
|
-
): Promise<void>
|
|
213
|
-
export async function implement(
|
|
214
|
-
first?: string | ImplementOptions,
|
|
215
|
-
tier?: RiskTier,
|
|
216
|
-
amend?: boolean,
|
|
217
|
-
): Promise<void> {
|
|
218
|
-
await implementing(
|
|
219
|
-
typeof first === "object" && first !== null ? first : { reference: first, tier, amend },
|
|
220
|
-
)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** `implement`, resolving with the criterion IDs the requirements listed when it closed. */
|
|
224
|
-
export const implementing = (options: ImplementOptions): Promise<readonly string[]> =>
|
|
225
|
-
scope("implement", async () => {
|
|
226
|
-
const built = options.naive === undefined || !(await applied(options.naive))
|
|
227
|
-
if (built) await build(options)
|
|
228
|
-
else requireCriteriaFrozen(REQUIREMENTS)
|
|
229
|
-
await green()
|
|
230
|
-
const ids = criterionIds(read(REQUIREMENTS) ?? "")
|
|
231
|
-
await run("closing", removeScript([REQUIREMENTS, REFACTOR_NOTE]), {
|
|
232
|
-
label: "Closing out the implementation",
|
|
233
|
-
})
|
|
234
|
-
return ids
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
interface Decision {
|
|
238
|
-
readonly reference: string
|
|
239
|
-
readonly tip: KeptTip
|
|
240
|
-
/** Absent on `land` (or unjudged, with no release flag asked for): the pass itself is the implementation. */
|
|
241
|
-
readonly shape?: DiffShapeResult
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
|
|
245
|
-
const steer = async (retry: t.NaiveRetry): Promise<boolean> => {
|
|
246
|
-
await human("steer", {
|
|
247
|
-
message: t.seamSteerMessage(retry, flagged()),
|
|
248
|
-
label: "Steer or overrule the naive pass",
|
|
249
|
-
file: REQUIREMENTS,
|
|
250
|
-
acceptClean: true,
|
|
251
|
-
})
|
|
252
|
-
if (changes().length === 0) return false
|
|
253
|
-
requireCriteria(REQUIREMENTS)
|
|
254
|
-
return true
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
type Verdict =
|
|
258
|
-
| { readonly kind: "retry"; readonly retry: t.NaiveRetry }
|
|
259
|
-
| { readonly kind: "steered" }
|
|
260
|
-
| { readonly kind: "overruled" }
|
|
261
|
-
|
|
262
|
-
/** Rejects, test faults and red checks share one budget; a steer or an overrule refills it. */
|
|
263
|
-
const budget = (): ((next: t.NaiveRetry) => Promise<Verdict>) => {
|
|
264
|
-
let retries = 0
|
|
265
|
-
return async (next) => {
|
|
266
|
-
if (retries < NAIVE_RETRIES) {
|
|
267
|
-
retries++
|
|
268
|
-
return { kind: "retry", retry: next }
|
|
269
|
-
}
|
|
270
|
-
const steered = await steer(next)
|
|
271
|
-
retries = 0
|
|
272
|
-
// A steered or overruled red pass leaves its report behind: nothing reads it any more.
|
|
273
|
-
if (read(FEEDBACK) !== undefined)
|
|
274
|
-
await run("clear", removeScript([FEEDBACK]), { label: "Clearing the red check" })
|
|
275
|
-
return { kind: steered ? "steered" : "overruled" }
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
interface Vetting {
|
|
280
|
-
again: ReturnType<typeof budget>
|
|
281
|
-
/** Set once a human overrules a test fault: the rule does not fit this change, so later passes skip it. */
|
|
282
|
-
waived: boolean
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/** Sends a pass whose tests fall short or run red back (`undefined`: it goes on, an overruled one as it is). */
|
|
286
|
-
const vetted = async (tip: KeptTip, vetting: Vetting): Promise<Verdict | undefined> => {
|
|
287
|
-
const fault = vetting.waived ? undefined : acceptanceFault(tip)
|
|
288
|
-
if (fault !== undefined) {
|
|
289
|
-
const verdict = await vetting.again({ fault, hash: tip.hash })
|
|
290
|
-
if (verdict.kind !== "overruled") return verdict
|
|
291
|
-
vetting.waived = true
|
|
292
|
-
}
|
|
293
|
-
if (await naiveGreen(tip)) return undefined
|
|
294
|
-
const verdict = await vetting.again({ red: tip.hash })
|
|
295
|
-
return verdict.kind === "overruled" ? undefined : verdict
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/** The judge's ruling on a vetted pass: the decision, or the next attempt's verdict. */
|
|
299
|
-
const ruled = async (tip: KeptTip, again: Vetting["again"]): Promise<Decision | Verdict> => {
|
|
300
|
-
const shape = await judgeSeam(tip)
|
|
301
|
-
if (shape.choice === "land") return { reference: tip.hash, tip }
|
|
302
|
-
if (shape.choice === "refactor-first") return { reference: tip.hash, tip, shape }
|
|
303
|
-
const next = await again({ strays: shape.strays, p: shape.judgment?.answers.shape?.p ?? 0 })
|
|
304
|
-
return next.kind === "overruled" ? { reference: tip.hash, tip } : next
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/** Back to `before`: every code path, and the notes file. `.gtd/` edits (the requirements) stay. */
|
|
308
|
-
const untryScript = (before: string): string => {
|
|
309
|
-
const code = changesSince(before).filter((c) => isCode(c.path))
|
|
310
|
-
return restoreScript(
|
|
311
|
-
head(),
|
|
312
|
-
{
|
|
313
|
-
restore: code.filter((c) => c.status !== "added").map((c) => c.path),
|
|
314
|
-
remove: [...code.filter((c) => c.status === "added").map((c) => c.path), NAIVE_TEST],
|
|
315
|
-
},
|
|
316
|
-
before,
|
|
317
|
-
)
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* A person tries the green pass `tip` in the working tree. Resolves
|
|
322
|
-
* `undefined` on a clean landing (signed off), else what they sent back;
|
|
323
|
-
* either way the pass, their code edits and the notes file leave the tree.
|
|
324
|
-
*/
|
|
325
|
-
const tried = async (tip: KeptTip): Promise<t.NaiveTested | undefined> => {
|
|
326
|
-
const before = head()
|
|
327
|
-
await run("applying", applyScript(tip.hash, FEEDBACK, t.naiveApplyFailure(tip.hash), [".gtd"]), {
|
|
328
|
-
label: "Applying the naive pass to try it",
|
|
329
|
-
})
|
|
330
|
-
await human("try", {
|
|
331
|
-
message: t.naiveTryMessage(flagged()),
|
|
332
|
-
label: "Try the naive pass",
|
|
333
|
-
file: NAIVE_TEST,
|
|
334
|
-
acceptClean: true,
|
|
335
|
-
})
|
|
336
|
-
const landed = changes()
|
|
337
|
-
const edits = landed.some((c) => isCode(c.path)) ? head() : undefined
|
|
338
|
-
const notes = read(NAIVE_TEST)?.trim() || undefined
|
|
339
|
-
if (landed.some((c) => c.path === REQUIREMENTS)) requireCriteria(REQUIREMENTS)
|
|
340
|
-
await run("restoring", untryScript(before), { label: "Taking the naive pass out again" })
|
|
341
|
-
return landed.length === 0 ? undefined : { tested: tip.hash, notes, edits }
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const decide = (): Promise<Decision> =>
|
|
345
|
-
scope("seam", async () => {
|
|
346
|
-
const vetting: Vetting = { again: budget(), waived: false }
|
|
347
|
-
let retry: t.NaiveRetry | t.NaiveTested | undefined
|
|
348
|
-
for (;;) {
|
|
349
|
-
await naivePass(retry)
|
|
350
|
-
const tip = kept("naive")!
|
|
351
|
-
const verdict = await vetted(tip, vetting)
|
|
352
|
-
if (verdict === undefined && naiveTestOn()) {
|
|
353
|
-
const sentBack = await tried(tip)
|
|
354
|
-
if (sentBack !== undefined) {
|
|
355
|
-
// A person's feedback is no failed attempt: the next pass gets a full retry budget.
|
|
356
|
-
vetting.again = budget()
|
|
357
|
-
retry = sentBack
|
|
358
|
-
continue
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
const outcome =
|
|
362
|
-
verdict ?? (flagged() ? await ruled(tip, vetting.again) : { reference: tip.hash, tip })
|
|
363
|
-
if (!("kind" in outcome)) return outcome
|
|
364
|
-
retry = outcome.kind === "retry" ? outcome.retry : undefined
|
|
365
|
-
}
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
export interface SeamOutcome {
|
|
369
|
-
/** The naive pass's kept tip: applied as the implementation when `land`, else the first implement turn's reading material. */
|
|
370
|
-
readonly reference: string
|
|
371
|
-
/** The naive pass lands as it is: no build turn. */
|
|
372
|
-
readonly land?: boolean
|
|
373
|
-
/** The feature's base: HEAD after the refactor on refactor-first, else the caller's base. */
|
|
374
|
-
readonly base: string
|
|
375
|
-
readonly refactored?: boolean
|
|
376
|
-
/** Set on refactor-first: what the rebuild carries over from the naive pass (the note only after a refactor). */
|
|
377
|
-
readonly rebuild?: t.Rebuild
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/** Write the refactor note from the whole naive pass; a turn that changes anything else is refused. */
|
|
381
|
-
export const refactorNote = async (
|
|
382
|
-
reference: string,
|
|
383
|
-
strays: readonly string[],
|
|
384
|
-
): Promise<string> => {
|
|
385
|
-
await agent("note", t.refactorNotePrompt(reference, strays), {
|
|
386
|
-
label: "Writing the refactor note",
|
|
387
|
-
file: REFACTOR_NOTE,
|
|
388
|
-
model: coder(),
|
|
389
|
-
system: t.builderSystem(),
|
|
390
|
-
})
|
|
391
|
-
const others = changes()
|
|
392
|
-
.map((c) => c.path)
|
|
393
|
-
.filter((path) => path !== REFACTOR_NOTE)
|
|
394
|
-
const note = read(REFACTOR_NOTE) ?? ""
|
|
395
|
-
if (others.length > 0 || note.trim() === "")
|
|
396
|
-
refuse(
|
|
397
|
-
`gtd land: refactor-note: write ${REFACTOR_NOTE} and change nothing else${others.length > 0 ? ` — revert these:\n${others.map((path) => ` - ${path}`).join("\n")}` : ""}`,
|
|
398
|
-
)
|
|
399
|
-
return note
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
export const seamBuild = async (base: string): Promise<SeamOutcome> => {
|
|
403
|
-
const { reference, tip, shape } = await decide()
|
|
404
|
-
if (shape === undefined) return { reference, base, land: true }
|
|
405
|
-
if (shape.unreadable.length > 0) {
|
|
406
|
-
await scope("seam", () =>
|
|
407
|
-
human("by-hand", {
|
|
408
|
-
message: t.seamByHandMessage(shape.strays, shape.unreadable, reference),
|
|
409
|
-
label: "Reviewing unreadable files by hand",
|
|
410
|
-
acceptClean: true,
|
|
411
|
-
}),
|
|
412
|
-
)
|
|
413
|
-
return { reference, base, rebuild: { acceptance: naiveAcceptance(tip) } }
|
|
414
|
-
}
|
|
415
|
-
const strays = refactorStrays(shape)
|
|
416
|
-
if (strays.length === 0) return { reference, base, rebuild: { acceptance: naiveAcceptance(tip) } }
|
|
417
|
-
const note = await scope("seam", () => refactorNote(reference, strays))
|
|
418
|
-
const refactored = await reviewedRefactor(shape, { reference, note })
|
|
419
|
-
return {
|
|
420
|
-
reference,
|
|
421
|
-
base: head(),
|
|
422
|
-
refactored,
|
|
423
|
-
// Fixed at `seam.note`, like the package's scope: a later edit of the file reaches neither turn.
|
|
424
|
-
rebuild: { note, acceptance: naiveAcceptance(tip) },
|
|
425
|
-
}
|
|
426
|
-
}
|