@pmelab/gtd 18.0.0 → 19.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 +34 -30
- package/claude/hooks/access.ts +58 -0
- package/claude/hooks/drive.ts +42 -4
- package/claude/hooks/register.tsx +12 -0
- package/dist/gtd.bundle.mjs +779 -668
- package/package.json +2 -1
- package/schema.json +22 -0
- package/skills/authoring/SKILL.md +6 -7
- package/src/flows/helpers.ts +4 -2
- package/src/flows/runtime.ts +20 -0
- package/src/flows/scripts.test.ts +28 -1
- package/src/flows/scripts.ts +3 -0
- package/src/workflows/access.test.ts +40 -0
- package/src/workflows/access.ts +22 -0
- package/src/workflows/health.test.ts +21 -1
- package/src/workflows/health.ts +47 -8
- package/src/workflows/packages.test.ts +91 -0
- package/src/workflows/packages.ts +96 -76
- package/src/workflows/planning.ts +5 -49
- package/src/workflows/prose.ts +18 -9
- package/src/workflows/review.test.ts +88 -1
- package/src/workflows/review.ts +41 -16
- package/src/workflows/scenarios.test.ts +256 -0
- package/src/workflows/scenarios.ts +137 -0
- package/src/workflows/skills.test.ts +0 -4
- package/src/workflows/skills.ts +0 -2
- package/src/workflows/steps.test.ts +0 -10
- package/src/workflows/steps.ts +2 -20
- package/src/workflows/text.fixture.ts +26 -0
- package/src/workflows/text.test.ts +101 -0
- package/src/workflows/text.ts +125 -97
- package/src/workflows/unified.ts +9 -4
- package/src/workflows/vars.ts +1 -2
- package/src/workflows/diff.test.ts +0 -115
- package/src/workflows/diff.ts +0 -306
package/src/workflows/text.ts
CHANGED
|
@@ -3,12 +3,15 @@ import {
|
|
|
3
3
|
codeThreads,
|
|
4
4
|
env,
|
|
5
5
|
head,
|
|
6
|
+
quote,
|
|
7
|
+
accessFor,
|
|
6
8
|
skillsFor,
|
|
7
9
|
start,
|
|
8
10
|
type AgentOptions,
|
|
9
11
|
type SummaryContext,
|
|
10
12
|
} from "../flows/index.js"
|
|
11
13
|
import {
|
|
14
|
+
accessPreamble,
|
|
12
15
|
skillsPreamble,
|
|
13
16
|
styleBlock,
|
|
14
17
|
styleFormatContract,
|
|
@@ -16,7 +19,6 @@ import {
|
|
|
16
19
|
designPersona,
|
|
17
20
|
architectPersona,
|
|
18
21
|
reviewerPersona,
|
|
19
|
-
specReviewerPersona,
|
|
20
22
|
builderPersona,
|
|
21
23
|
finisherPersona,
|
|
22
24
|
escalationPersona,
|
|
@@ -37,12 +39,16 @@ export const withSkills = (skills: string | undefined, prompt: string): string =
|
|
|
37
39
|
return `${skillsPreamble.replaceAll("{skills}", skills)}\n\n${prompt}`
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
/** An `agent()` step whose prompt opens with the preamble naming its scope's skills. */
|
|
42
|
+
/** An `agent()` step whose prompt opens with the preamble naming its scope's skills, then its restricted file access. */
|
|
41
43
|
export const agentWithSkills = (
|
|
42
44
|
name: string,
|
|
43
45
|
prompt: string,
|
|
44
46
|
options: AgentOptions = {},
|
|
45
|
-
): Promise<void> =>
|
|
47
|
+
): Promise<void> => {
|
|
48
|
+
const access = accessPreamble(accessFor(name, options.file))
|
|
49
|
+
const body = access === "" ? prompt : `${access}\n\n${prompt}`
|
|
50
|
+
return agent(name, withSkills(skillsFor(name).join(", "), body), options)
|
|
51
|
+
}
|
|
46
52
|
|
|
47
53
|
export const unwindFailure = (commit: string): string =>
|
|
48
54
|
`gtd could not unwind ${commit} out of your working tree.`
|
|
@@ -76,16 +82,6 @@ What each change does next (then run \`gtd land\`):
|
|
|
76
82
|
- **Continue** — having undone the sketch by hand, check the test baseline is green and start triage (**start-gate.check**).
|
|
77
83
|
`
|
|
78
84
|
|
|
79
|
-
export const architecturePreMessage = (): string =>
|
|
80
|
-
`Judging whether \`.gtd/REQUIREMENTS.md\`'s settled concerns need a
|
|
81
|
-
dedicated architecture pass — real structural decisions, multiple
|
|
82
|
-
integration points, or a non-obvious tradeoff — before packages
|
|
83
|
-
are written. Run \`gtd judge answer\` and pipe a verdict for
|
|
84
|
-
\`architectureWarranted\` — or land untouched to run the full pass
|
|
85
|
-
(the conservative default; a skipped judgment never suppresses
|
|
86
|
-
it).
|
|
87
|
-
`
|
|
88
|
-
|
|
89
85
|
export const startGateBlockedMessage = (): string =>
|
|
90
86
|
`The test baseline is red — gtd will not start new work on a broken suite.
|
|
91
87
|
\`.gtd/FEEDBACK.md\` holds the failing output.
|
|
@@ -263,22 +259,46 @@ ${footnoteFoldIn}${codeThreadReplies()}
|
|
|
263
259
|
authority is to merge only, never to split — the whole problem
|
|
264
260
|
is over-granularity
|
|
265
261
|
- Record every merge under \`## Merged Concerns\`,
|
|
266
|
-
carrying both merged requirements verbatim so
|
|
267
|
-
|
|
262
|
+
carrying both merged requirements verbatim so each requirement
|
|
263
|
+
stays traceable to its tests
|
|
268
264
|
- A merge raises no open question and stops for no
|
|
269
265
|
human — do not route it to \`architecture.gate\` for a veto; the
|
|
270
|
-
human sees it when reviewing the plan
|
|
271
|
-
real safety net
|
|
266
|
+
human sees it when reviewing the plan
|
|
272
267
|
- Prefer fewer, larger packages — the smallest independently
|
|
273
268
|
valuable change, not the smallest change that compiles
|
|
274
269
|
- Every open point here is TECHNICAL — triage already resolved
|
|
275
270
|
the product ones, one phase earlier. PERMISSIVE: answer it
|
|
276
271
|
yourself unless you genuinely cannot defend a default; a wrong
|
|
277
|
-
technical call is still caught
|
|
272
|
+
technical call is still caught by the full run and the quality lap
|
|
278
273
|
- The narrow exception: a simplification that drops something
|
|
279
274
|
\`.gtd/REQUIREMENTS.md\` mentions is an open question, not a
|
|
280
275
|
silent default
|
|
281
276
|
|
|
277
|
+
- \`.gtd/ARCHITECTURE.md\` is the acceptance spec the human signs
|
|
278
|
+
off. It carries these sections, in this order:
|
|
279
|
+
1. \`## Open Questions\` (qa format, first, only when present)
|
|
280
|
+
2. \`## Interfaces\` — new and changed signatures/contracts
|
|
281
|
+
only, as fenced TypeScript (or the repository's language)
|
|
282
|
+
3. \`## Call Stacks\` — seam level, entry point → module
|
|
283
|
+
boundary; one line per hop, never per line of code
|
|
284
|
+
4. \`## E2E Scenarios\` — Gherkin in fenced \`gherkin\` blocks
|
|
285
|
+
(whatever e2e framework the repository uses), each preceded
|
|
286
|
+
by a line \`- e2e: <path>\`. With no user-visible
|
|
287
|
+
behaviour change, the literal line \`No e2e change.\` plus a
|
|
288
|
+
one-line reason — never empty or missing
|
|
289
|
+
5. \`## Unit Tests\` — one \`### <interface>\` per entry of
|
|
290
|
+
\`## Interfaces\`, each test a line
|
|
291
|
+
\`- unit: <path> — <behaviour> (covers <concern>)\`;
|
|
292
|
+
never against an internal helper. A requirement no test can
|
|
293
|
+
cover (docs, README, deletions, config) goes under a
|
|
294
|
+
\`### Chores\` heading in this section as
|
|
295
|
+
\`- chore: <path> — <what> (covers <concern>)\`
|
|
296
|
+
6. \`## Merged Concerns\` — kept, when any merge happened
|
|
297
|
+
7. \`## Answered Questions\` — last
|
|
298
|
+
- Coverage rule: every concern in \`.gtd/REQUIREMENTS.md\` appears
|
|
299
|
+
in at least one \`(covers …)\` tail, as a test or a chore. Each
|
|
300
|
+
test entry carries its file path and level (unit / e2e)
|
|
301
|
+
|
|
282
302
|
${questionBar}
|
|
283
303
|
## Return lap
|
|
284
304
|
|
|
@@ -294,26 +314,41 @@ export const architectureDecomposePrompt = (): string =>
|
|
|
294
314
|
|
|
295
315
|
${stateFileRules}
|
|
296
316
|
- The only state files this turn touches are the package files
|
|
297
|
-
under \`.gtd/packages/\`
|
|
298
|
-
|
|
317
|
+
under \`.gtd/packages/\` — no other files for notes or output.
|
|
318
|
+
Never delete \`.gtd/ARCHITECTURE.md\`: the build tail's full run
|
|
319
|
+
still needs it, and package files may reference it
|
|
299
320
|
- Work from \`.gtd/ARCHITECTURE.md\` if you wrote it earlier this
|
|
300
321
|
conversation, otherwise read it (the converged technical
|
|
301
|
-
plan). It already lists
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
322
|
+
plan). It already lists the concerns, interfaces, e2e scenarios
|
|
323
|
+
and unit tests — this turn groups them into packages. A
|
|
324
|
+
\`## Merged Concerns\` heading there records merges, never a
|
|
325
|
+
concern of its own: write no package file for it
|
|
326
|
+
- A package is a consecutive run of \`## Unit Tests\` entries that go
|
|
327
|
+
green together on the fast suite, at most ~8 unit-test entries
|
|
328
|
+
each. Keep the merge rule: concerns whose footprints centre on the
|
|
329
|
+
same files stay in one package unless the later one only consumes
|
|
330
|
+
an interface the earlier one creates. Prefer fewer, larger
|
|
331
|
+
packages. Order them so each stays green on the fast suite —
|
|
332
|
+
interface-introducing packages first
|
|
333
|
+
- Write the packages under \`.gtd/packages/\`, each carrying its
|
|
334
|
+
requirement(s) — both, independently, if merged — its tasks, and
|
|
335
|
+
each task's acceptance criteria as \`- [ ]\` checkboxes and
|
|
336
|
+
relevant paths:
|
|
337
|
+
- \`.gtd/packages/00-e2e-scenarios.md\` — package 0: it writes
|
|
338
|
+
\`## E2E Scenarios\` as tests in the repository's own e2e
|
|
339
|
+
framework and leaves them red. Write no such file when
|
|
340
|
+
\`## E2E Scenarios\` says "No e2e change."
|
|
341
|
+
- \`01-…\` onward — the unit-test packages. A requirement no test
|
|
342
|
+
covers (docs, README, deletions, config) becomes a chore
|
|
343
|
+
package, or a task of one; chore packages declare no tests
|
|
344
|
+
- the last numbered package is the wiring package: CLI entry,
|
|
345
|
+
workflow composition, config. A red e2e after it is a bug, not
|
|
346
|
+
missing work
|
|
347
|
+
- Every package with tests has a \`## Tests\` section, one line per
|
|
348
|
+
declared test, copied from the architecture entries:
|
|
349
|
+
\`- unit: \\\`<path>\\\`\` or \`- e2e: \\\`<path>\\\`\`. Chore packages have
|
|
350
|
+
none
|
|
351
|
+
- Leave everything uncommitted and finish
|
|
317
352
|
`
|
|
318
353
|
|
|
319
354
|
export const architectureGateAnswerMessage = (): string =>
|
|
@@ -346,7 +381,7 @@ What each change does next (then run \`gtd land\`):
|
|
|
346
381
|
export const packagesItemBuildingPrompt = (pkg: string): string =>
|
|
347
382
|
`${stateFileRules}
|
|
348
383
|
- The only state file this turn may write is \`.gtd/SATISFIED.md\`;
|
|
349
|
-
never delete the package file (the
|
|
384
|
+
never delete the package file (the declared-tests guard reads it
|
|
350
385
|
after you)
|
|
351
386
|
- The package to implement is \`${pkg}\`
|
|
352
387
|
- First check its acceptance criteria against the current tree —
|
|
@@ -355,6 +390,13 @@ export const packagesItemBuildingPrompt = (pkg: string): string =>
|
|
|
355
390
|
with each criterion's concrete evidence (commit, file, or
|
|
356
391
|
symbol), change nothing else, and finish. Otherwise implement
|
|
357
392
|
normally and skip that file
|
|
393
|
+
- Write the tests the package declares under \`## Tests\` first,
|
|
394
|
+
then the code that makes them pass; the turn is refused unless
|
|
395
|
+
every declared test is in your diff
|
|
396
|
+
- Package 0 (\`.gtd/packages/00-e2e-scenarios.md\`) writes the scenarios verbatim
|
|
397
|
+
from \`## E2E Scenarios\` of \`.gtd/ARCHITECTURE.md\`, with only
|
|
398
|
+
step code that typechecks, and leaves them red — no
|
|
399
|
+
implementation
|
|
358
400
|
- Implement every task the package describes, no more, no less,
|
|
359
401
|
fanning independent ones out to parallel subagents where your
|
|
360
402
|
harness supports it; leave other package files untouched
|
|
@@ -376,17 +418,6 @@ ${fixFeedbackPrompt}
|
|
|
376
418
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
377
419
|
`
|
|
378
420
|
|
|
379
|
-
export const packagesItemFixSpecPrompt = (pkg: string): string =>
|
|
380
|
-
`${stateFileRules}
|
|
381
|
-
- The only state file this turn touches is
|
|
382
|
-
\`.gtd/SPEC_FEEDBACK.md\` — address it, then delete it
|
|
383
|
-
- Read it (the reviewer's concerns) and the package spec
|
|
384
|
-
(\`${pkg}\`), then fix the code to
|
|
385
|
-
resolve every concern
|
|
386
|
-
- Delete \`.gtd/SPEC_FEEDBACK.md\` once resolved; leave everything
|
|
387
|
-
else uncommitted and finish your turn
|
|
388
|
-
`
|
|
389
|
-
|
|
390
421
|
export const healthJudgeMessage = (): string =>
|
|
391
422
|
`The check is still red, and this isn't the first round —
|
|
392
423
|
\`.gtd/FEEDBACK.md\` holds this round's output, and the previous round's
|
|
@@ -434,59 +465,34 @@ turn, or land untouched to give it one more attempt at the same
|
|
|
434
465
|
analysis.
|
|
435
466
|
`
|
|
436
467
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
/** The sections a pre-judge could not clear, when it cleared the rest. */
|
|
446
|
-
const specScope = (failing: readonly string[]): string =>
|
|
447
|
-
failing.length > 0
|
|
448
|
-
? `- A pre-judge already found the other sections satisfied. Confine
|
|
449
|
-
your review to only these sections:
|
|
450
|
-
${failing.map((title) => ` - ${title}\n`).join("")}`
|
|
451
|
-
: ""
|
|
452
|
-
|
|
453
|
-
export const packagesItemSpecReviewPrompt = (
|
|
454
|
-
pkg: string,
|
|
455
|
-
failing: readonly string[] = [],
|
|
456
|
-
): string =>
|
|
457
|
-
`${styleBlock}
|
|
458
|
-
|
|
459
|
-
You are reviewing a freshly-built work package against its own
|
|
460
|
-
spec.
|
|
461
|
-
|
|
462
|
-
${stateFileRules}
|
|
463
|
-
- The only state file this turn touches is
|
|
464
|
-
\`.gtd/SPEC_FEEDBACK.md\` — write it only when you find problems
|
|
465
|
-
- The package spec is \`${pkg}\`
|
|
466
|
-
${specScope(failing)}- Verify the implementation against it: tasks done, criteria
|
|
467
|
-
met, code sound and consistent with the codebase. No diff is
|
|
468
|
-
given — read the range yourself, from \`${start()}\`
|
|
469
|
-
to the working tree, process-wide (it can span earlier
|
|
470
|
-
packages)
|
|
471
|
-
- You own that bar; nothing downstream re-weighs your findings
|
|
472
|
-
- Write nothing when the package fully satisfies its spec —
|
|
473
|
-
silence is your approval. Otherwise write
|
|
474
|
-
\`.gtd/SPEC_FEEDBACK.md\` listing what would violate the spec if
|
|
475
|
-
it shipped unaddressed, specific enough to act on, each as its
|
|
476
|
-
own \`## \` heading
|
|
477
|
-
- Never fix anything yourself and never delete the package
|
|
478
|
-
file — a later step owns that
|
|
479
|
-
`
|
|
480
|
-
|
|
481
|
-
export const specReviewerSystem = (): string =>
|
|
482
|
-
`${specReviewerPersona}
|
|
468
|
+
/** What the tail's full-run fix needs of a `BuiltPlan` (structural: `packages.ts` imports this module). */
|
|
469
|
+
export interface BuildContext {
|
|
470
|
+
readonly ranges: readonly { readonly pkg: string; readonly from: string; readonly to: string }[]
|
|
471
|
+
readonly scenarios: {
|
|
472
|
+
readonly added: readonly string[]
|
|
473
|
+
readonly changed: readonly string[]
|
|
474
|
+
}
|
|
475
|
+
}
|
|
483
476
|
|
|
484
|
-
|
|
477
|
+
const buildContextPrompt = (built: BuildContext): string => {
|
|
478
|
+
const list = (paths: readonly string[]): string =>
|
|
479
|
+
paths.length === 0 ? " (none)" : paths.map((path) => ` - ${path}`).join("\n")
|
|
480
|
+
return `- Read \`.gtd/ARCHITECTURE.md\` — the plan every package was built from
|
|
481
|
+
- A red scenario that is NEW in this plan likely means missing wiring
|
|
482
|
+
between packages; new scenarios:
|
|
483
|
+
${list(built.scenarios.added)}
|
|
484
|
+
- A red CHANGED or existing scenario or test is a regression a package
|
|
485
|
+
introduced; scenarios this plan changed:
|
|
486
|
+
${list(built.scenarios.changed)}
|
|
487
|
+
- Each package's commits, to bisect a break:
|
|
488
|
+
${built.ranges.map((r) => ` git log --oneline ${r.from}..${r.to} # ${r.pkg}`).join("\n")}
|
|
489
|
+
`
|
|
490
|
+
}
|
|
485
491
|
|
|
486
|
-
export const buildFixPrompt = (): string =>
|
|
492
|
+
export const buildFixPrompt = (built?: BuildContext): string =>
|
|
487
493
|
`${stateFileRules}
|
|
488
494
|
${fixFeedbackPrompt}
|
|
489
|
-
- Leave everything uncommitted — do not commit
|
|
495
|
+
${built === undefined ? "" : buildContextPrompt(built)}- Leave everything uncommitted — do not commit
|
|
490
496
|
`
|
|
491
497
|
|
|
492
498
|
export const finisherSystem = (): string =>
|
|
@@ -877,3 +883,25 @@ ${it.processCostByModel.map((m) => `- ${m.model}: ${m.cost}\n`).join("")}
|
|
|
877
883
|
Print the closing message and stop — this writes nothing itself.
|
|
878
884
|
`
|
|
879
885
|
}
|
|
886
|
+
|
|
887
|
+
export interface WordingDrift {
|
|
888
|
+
readonly path: string
|
|
889
|
+
/** `- `/`+ `-prefixed, in file order. */
|
|
890
|
+
readonly lines: readonly string[]
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
export const scenarioWordingMessage = (at: string, drifted: readonly WordingDrift[]): string =>
|
|
894
|
+
`The e2e scenario wording was frozen when package 0 landed, and the last
|
|
895
|
+
turn changed it. Removed lines are marked \`-\`, added ones \`+\`:
|
|
896
|
+
|
|
897
|
+
${drifted.map(({ path, lines }) => `${path}\n${lines.join("\n")}`).join("\n\n")}
|
|
898
|
+
|
|
899
|
+
To accept the change, land untouched.
|
|
900
|
+
|
|
901
|
+
To reject it, run
|
|
902
|
+
|
|
903
|
+
git checkout ${at} -- ${drifted.map(({ path }) => quote(path)).join(" ")}
|
|
904
|
+
|
|
905
|
+
and land. Restoring only some of the files is a partial accept: every file
|
|
906
|
+
still differing from the frozen wording is accepted.
|
|
907
|
+
`
|
package/src/workflows/unified.ts
CHANGED
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
} from "../flows/index.js"
|
|
15
15
|
import { baseline, gate } from "./health.js"
|
|
16
16
|
import { packages } from "./packages.js"
|
|
17
|
-
import {
|
|
17
|
+
import { architecture, design } from "./planning.js"
|
|
18
|
+
import type { FrozenScenarios } from "./scenarios.js"
|
|
18
19
|
import { buildTail, type ReviewOutcome } from "./review.js"
|
|
19
20
|
import { ARCHITECTURE, FEEDBACK, REQUIREMENTS, REVIEW } from "./steps.js"
|
|
20
21
|
import * as t from "./text.js"
|
|
@@ -33,11 +34,13 @@ import * as t from "./text.js"
|
|
|
33
34
|
export { threads, type ThreadInfo } from "../flows/index.js"
|
|
34
35
|
export { defaults, envDefaults } from "./vars.js"
|
|
35
36
|
export { skills } from "./skills.js"
|
|
37
|
+
export { access } from "./access.js"
|
|
36
38
|
export { agentWithSkills } from "./text.js"
|
|
37
39
|
export * from "./steps.js"
|
|
38
40
|
export * from "./health.js"
|
|
39
41
|
export * from "./planning.js"
|
|
40
42
|
export * from "./packages.js"
|
|
43
|
+
export * from "./scenarios.js"
|
|
41
44
|
export * from "./review.js"
|
|
42
45
|
|
|
43
46
|
/** Revert the sketch that started the process out of the working tree; its intent survives in history. */
|
|
@@ -69,11 +72,13 @@ export const reUnwind = async (
|
|
|
69
72
|
/** Plan, build and review until a review round signs off; feedback re-plans from scratch. */
|
|
70
73
|
export const planAndBuild = async (firstBase: string): Promise<void> => {
|
|
71
74
|
let base = firstBase
|
|
75
|
+
let carried: FrozenScenarios | undefined
|
|
72
76
|
for (;;) {
|
|
73
77
|
await design(base)
|
|
74
|
-
await
|
|
75
|
-
await packages()
|
|
76
|
-
|
|
78
|
+
await architecture()
|
|
79
|
+
const built = await packages(carried)
|
|
80
|
+
carried = built.frozen
|
|
81
|
+
const outcome = await buildTail(false, base, built)
|
|
77
82
|
if (outcome.verdict === "signoff") return
|
|
78
83
|
await reUnwind(outcome)
|
|
79
84
|
base = outcome.base
|
package/src/workflows/vars.ts
CHANGED
|
@@ -9,9 +9,7 @@ export const defaults: Readonly<Record<string, string>> = {
|
|
|
9
9
|
reviewBase: "",
|
|
10
10
|
judgeBudgetBytes: "32768",
|
|
11
11
|
judgeIdenticalMinP: "0.7",
|
|
12
|
-
specPreJudge: "0.9",
|
|
13
12
|
reviewNoteActionable: "0.7",
|
|
14
|
-
architectureSkipMinP: "0.85",
|
|
15
13
|
// Decides the turn count: one `build.quality.<lens>` scope per entry, each
|
|
16
14
|
// keyed in `./skills.ts`.
|
|
17
15
|
qualityReviews:
|
|
@@ -21,6 +19,7 @@ export const defaults: Readonly<Record<string, string>> = {
|
|
|
21
19
|
/** Environment settings. */
|
|
22
20
|
export const envDefaults: Readonly<Record<string, string>> = {
|
|
23
21
|
testCommand: "npm test",
|
|
22
|
+
fastTestCommand: "",
|
|
24
23
|
plannerModel: "smart",
|
|
25
24
|
coderModel: "base",
|
|
26
25
|
}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest"
|
|
2
|
-
import type { Change } from "../flows/index.js"
|
|
3
|
-
import { filterChanges, packageDiff } from "./diff.js"
|
|
4
|
-
|
|
5
|
-
const change = (over: Partial<Change> & Pick<Change, "path" | "status">): Change => ({
|
|
6
|
-
before: undefined,
|
|
7
|
-
after: undefined,
|
|
8
|
-
...over,
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
// A cap high enough that nothing is ever dropped, for cases exercising the
|
|
12
|
-
// rendering pipeline rather than the byte-budget behavior itself.
|
|
13
|
-
const NO_CAP = 10_000
|
|
14
|
-
|
|
15
|
-
describe("packageDiff / rendering", () => {
|
|
16
|
-
it("renders a normal edit as hunks with three lines of context", () => {
|
|
17
|
-
const before = Array.from({ length: 10 }, (_, i) => `line ${i + 1}`).join("\n")
|
|
18
|
-
const after = before.replace("line 5", "line five")
|
|
19
|
-
const text = packageDiff([change({ path: "a.ts", status: "modified", before, after })], NO_CAP)
|
|
20
|
-
expect(text).toContain("--- a/a.ts")
|
|
21
|
-
expect(text).toContain("+++ b/a.ts")
|
|
22
|
-
expect(text).toContain("-line 5")
|
|
23
|
-
expect(text).toContain("+line five")
|
|
24
|
-
// three lines of context on either side of the one changed line
|
|
25
|
-
expect(text).toContain(" line 2")
|
|
26
|
-
expect(text).toContain(" line 8")
|
|
27
|
-
expect(text).not.toContain(" line 1\n")
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it("degrades a 1500+ line change to its summary line", () => {
|
|
31
|
-
const before = Array.from({ length: 2000 }, (_, i) => `line ${i}`).join("\n")
|
|
32
|
-
const after = Array.from({ length: 2000 }, (_, i) => `changed ${i}`).join("\n")
|
|
33
|
-
const text = packageDiff(
|
|
34
|
-
[change({ path: "big.ts", status: "modified", before, after })],
|
|
35
|
-
NO_CAP,
|
|
36
|
-
)
|
|
37
|
-
expect(text).toBe("big.ts: +2000/-2000 lines, too large to inline")
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it("renders files in path order for byte-identical output across replays", () => {
|
|
41
|
-
const a = change({ path: "z.ts", status: "added", after: "z" })
|
|
42
|
-
const b = change({ path: "a.ts", status: "added", after: "a" })
|
|
43
|
-
expect(packageDiff([a, b], NO_CAP)).toBe(packageDiff([b, a], NO_CAP))
|
|
44
|
-
expect(packageDiff([a, b], NO_CAP).indexOf("a.ts")).toBeLessThan(
|
|
45
|
-
packageDiff([a, b], NO_CAP).indexOf("z.ts"),
|
|
46
|
-
)
|
|
47
|
-
})
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
describe("filterChanges / exclusion list", () => {
|
|
51
|
-
it("excludes a lockfile change entirely", () => {
|
|
52
|
-
const c = change({ path: "package-lock.json", status: "modified", before: "a", after: "b" })
|
|
53
|
-
expect(filterChanges([c])).toEqual([])
|
|
54
|
-
expect(packageDiff([c], NO_CAP)).toBe("")
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it("excludes .gtd/** paths", () => {
|
|
58
|
-
const c = change({ path: ".gtd/PLAN.md", status: "added", after: "x" })
|
|
59
|
-
expect(filterChanges([c])).toEqual([])
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it("excludes a binary file by extension", () => {
|
|
63
|
-
const c = change({ path: "logo.png", status: "added", after: "binary-ish" })
|
|
64
|
-
expect(filterChanges([c])).toEqual([])
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it("excludes a binary file by content, separately from extension", () => {
|
|
68
|
-
const c = change({ path: "weird.txt", status: "added", after: "abc\0def" })
|
|
69
|
-
expect(filterChanges([c])).toEqual([])
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it("keeps an ordinary source file", () => {
|
|
73
|
-
const c = change({ path: "src/a.ts", status: "added", after: "export const a = 1\n" })
|
|
74
|
-
expect(filterChanges([c])).toEqual([c])
|
|
75
|
-
})
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
describe("packageDiff", () => {
|
|
79
|
-
it("drops whole files from the end and names them in a trailer", () => {
|
|
80
|
-
const a = change({ path: "a.ts", status: "added", after: "export const a = 1\n" })
|
|
81
|
-
const b = change({
|
|
82
|
-
path: "b.ts",
|
|
83
|
-
status: "added",
|
|
84
|
-
after: Array.from({ length: 50 }, (_, i) => `line ${i}`).join("\n"),
|
|
85
|
-
})
|
|
86
|
-
const text = packageDiff([a, b], 120)
|
|
87
|
-
expect(text).toContain("--- /dev/null")
|
|
88
|
-
expect(text).toContain("+++ b/a.ts")
|
|
89
|
-
expect(text).not.toContain("b.ts\n")
|
|
90
|
-
expect(text).toContain("1 file(s) omitted for the judge's byte budget: b.ts")
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it("counts the omission trailer itself against the cap, so many dropped paths never push the total past it", () => {
|
|
94
|
-
const kept = change({ path: "a.ts", status: "added", after: "export const a = 1\n" })
|
|
95
|
-
const rest = Array.from({ length: 6 }, (_, i) =>
|
|
96
|
-
change({ path: `f${i}.ts`, status: "added", after: "y" }),
|
|
97
|
-
)
|
|
98
|
-
// A naive cap check (size the kept text alone, append the trailer
|
|
99
|
-
// afterwards) would keep a.ts plus two of the small files here — 150
|
|
100
|
-
// bytes of text — then tack on an unbudgeted trailer for the other four,
|
|
101
|
-
// landing well past capBytes. Budgeting the trailer itself must instead
|
|
102
|
-
// drop enough files that the total, trailer included, stays at or under it.
|
|
103
|
-
const capBytes = 150
|
|
104
|
-
const text = packageDiff([kept, ...rest], capBytes)
|
|
105
|
-
expect(Buffer.byteLength(text, "utf8")).toBeLessThanOrEqual(capBytes)
|
|
106
|
-
expect(text).toContain("omitted")
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it("keeps every file when the cap is not exceeded", () => {
|
|
110
|
-
const a = change({ path: "a.ts", status: "added", after: "x" })
|
|
111
|
-
const text = packageDiff([a], 10_000)
|
|
112
|
-
expect(text).not.toContain("omitted")
|
|
113
|
-
expect(text).toContain("a.ts")
|
|
114
|
-
})
|
|
115
|
-
})
|