@pmelab/gtd 23.2.0 → 24.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 +13 -6
- package/dist/gtd.bundle.mjs +34 -12
- package/dist/workflows.mjs +1952 -1725
- package/package.json +2 -1
- package/skills/authoring/SKILL.md +5 -4
- package/src/flows/factory.ts +5 -1
- package/src/flows/runtime.ts +3 -0
- package/src/workflow-kit/health.test.ts +30 -0
- package/src/workflow-kit/health.ts +86 -0
- package/src/workflow-kit/index.ts +7 -0
- package/src/workflow-kit/quality.test.ts +17 -0
- package/src/workflow-kit/quality.ts +45 -0
- package/src/workflow-kit/review.ts +324 -0
- package/src/{workflows → workflow-kit}/scenarios.ts +21 -28
- package/src/workflows/feature/feature.test.ts +38 -0
- package/src/workflows/{bundled.ts → feature/feature.ts} +11 -63
- package/src/workflows/{packages.test.ts → feature/packages.test.ts} +2 -2
- package/src/workflows/{packages.ts → feature/packages.ts} +3 -3
- package/src/workflows/{planning.ts → feature/planning.ts} +2 -3
- package/src/workflows/{review.test.ts → feature/review.test.ts} +9 -44
- package/src/workflows/feature/review.ts +106 -0
- package/src/workflows/feature/steps.test.ts +82 -0
- package/src/workflows/feature/steps.ts +92 -0
- package/src/workflows/fix/fix.test.ts +94 -0
- package/src/workflows/fix/fix.ts +52 -0
- package/src/workflows/index.ts +44 -2
- package/src/workflows/review/review.test.ts +43 -0
- package/src/workflows/review/review.ts +31 -0
- package/src/workflows/{access.test.ts → shared/access.test.ts} +1 -1
- package/src/workflows/{access.ts → shared/access.ts} +3 -5
- package/src/workflows/{health.test.ts → shared/health.test.ts} +1 -1
- package/src/workflows/{health.ts → shared/health.ts} +38 -55
- package/src/workflows/{options.test.ts → shared/options.test.ts} +2 -2
- package/src/workflows/{options.ts → shared/options.ts} +14 -2
- package/src/workflows/{scenarios.test.ts → shared/scenarios.test.ts} +1 -1
- package/src/workflows/shared/scenarios.ts +35 -0
- package/src/workflows/{skills.test.ts → shared/skills.test.ts} +3 -1
- package/src/workflows/{skills.ts → shared/skills.ts} +7 -6
- package/src/workflows/{steps.test.ts → shared/steps.test.ts} +1 -66
- package/src/workflows/{steps.ts → shared/steps.ts} +8 -98
- package/src/workflows/shared/tail.test.ts +205 -0
- package/src/workflows/shared/tail.ts +135 -0
- package/src/workflows/{text.fixture.ts → shared/text.fixture.ts} +2 -2
- package/src/workflows/{text.ts → shared/text.ts} +48 -7
- package/src/workflows/{threads.test.ts → shared/threads.test.ts} +1 -1
- package/src/workflows/review.ts +0 -362
- /package/src/workflows/{prose.ts → shared/prose.ts} +0 -0
- /package/src/workflows/{text.test.ts → shared/text.test.ts} +0 -0
- /package/src/workflows/{vars.ts → shared/vars.ts} +0 -0
package/dist/workflows.mjs
CHANGED
|
@@ -315,1297 +315,106 @@ const defineWorkflow = (schema, build) => {
|
|
|
315
315
|
return Object.assign(factory, { [FACTORY]: internals });
|
|
316
316
|
};
|
|
317
317
|
//#endregion
|
|
318
|
-
//#region src/
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
prompt states first
|
|
326
|
-
- Never let a loaded skill turn this turn interactive — answer nothing,
|
|
327
|
-
ask nothing; this runs unattended, with no one at a keyboard`;
|
|
328
|
-
/** One bullet per restricted side; empty when neither is. An empty glob list reads "nothing". */
|
|
329
|
-
const accessPreamble = (access) => {
|
|
330
|
-
const globs = (list) => list.length === 0 ? "nothing" : list.join(", ");
|
|
331
|
-
return [...access.read === null ? [] : [`- This turn may read only: ${globs(access.read)}`], ...access.write === null ? [] : [`- This turn may write only: ${globs(access.write)} — anything else is refused when the turn lands`]].join("\n");
|
|
318
|
+
//#region src/workflow-kit/health.ts
|
|
319
|
+
/** Hand a red suite that repeated attempts could not fix to a person, until `limit` rounds are spent. */
|
|
320
|
+
const escalation$1 = async (count, steps) => {
|
|
321
|
+
if (count.rounds >= steps.limit) return steps.exhausted();
|
|
322
|
+
count.rounds++;
|
|
323
|
+
await steps.describe();
|
|
324
|
+
await steps.stop();
|
|
332
325
|
};
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
classified list of concerns, and hold the running product conversation
|
|
371
|
-
with the human it depends on. You are read by that human — write
|
|
372
|
-
plainly — not by a parser, except where a state says otherwise.`;
|
|
373
|
-
const architectPersona = `You are the technical planning voice in gtd's build pipeline: take over
|
|
374
|
-
once product concerns are settled, reading them cold, no carried
|
|
375
|
-
conversation. Work out the *how* per concern — structure, data models,
|
|
376
|
-
tech-stack choices, error handling — raise the technical open
|
|
377
|
-
questions, then pin interfaces and tests in the architecture document.`;
|
|
378
|
-
const reviewerPersona = `You are the independent reviewing mind in gtd's build pipeline —
|
|
379
|
-
deliberately separate from whoever wrote the code, with no attachment
|
|
380
|
-
to it. Write a structured review document grouping a diff into
|
|
381
|
-
chunks; classify a round of the human's feedback as actionable or
|
|
382
|
-
just approving; answer the human's questions inline in the review;
|
|
383
|
-
and, when asked, fix the small nits the human flagged and the risks
|
|
384
|
-
you marked yourself, each in one batch.
|
|
385
|
-
Beyond that you never fix or build anything yourself.`;
|
|
386
|
-
const builderPersona = `You are the TDD implementer in gtd's build pipeline: build one
|
|
387
|
-
package's declared scope end to end, tests first, and come back to fix
|
|
388
|
-
things when redirected — a failing check, or reviewer feedback. Stay
|
|
389
|
-
strictly inside the package in front of you; never touch another
|
|
390
|
-
package's files or code outside the task. Treat feedback as the
|
|
391
|
-
diagnosis to act on; discard it, with reason, only when simply wrong.`;
|
|
392
|
-
const finisherPersona = `You are the closing identity in gtd's build pipeline — the last coder
|
|
393
|
-
before it closes. Fix a late-breaking failing check after sign-off,
|
|
394
|
-
focused and minimal. Every turn lands its own commit — nothing here
|
|
395
|
-
gets squashed away.`;
|
|
396
|
-
const escalationPersona = `You are the diagnosing voice in gtd's build pipeline, called in once a
|
|
397
|
-
check has stayed red past repeated fix attempts. Read the failing
|
|
398
|
-
output, the previous round's, and the code those attempts touched, then
|
|
399
|
-
write down what is failing, why the earlier attempts didn't resolve it,
|
|
400
|
-
and concrete approaches worth trying next. Never fix the code yourself —
|
|
401
|
-
naming the problem precisely enough for the next fix turn is the whole
|
|
402
|
-
job.`;
|
|
403
|
-
const stateFileRules = `- You are an autonomous coding agent
|
|
404
|
-
- This workflow's own state files are its private scratchpad, never
|
|
405
|
-
project code or documentation`;
|
|
406
|
-
const questionBar = `- The goal is shared understanding, not a quota or an empty section: a
|
|
407
|
-
question exists to close a gap between what the human wants and
|
|
408
|
-
what the agent is about to build. Ask whenever that gap is open —
|
|
409
|
-
even when the point looks cheap to undo, settled-looking, or
|
|
410
|
-
narrow. This goal outranks what follows; the conditions below are
|
|
411
|
-
signals a gap is real, strong evidence to ask, never permission
|
|
412
|
-
withheld
|
|
413
|
-
- Before writing \`## Open Questions\`, walk every concern you grouped and
|
|
414
|
-
collect every point above the bar into that ONE section — a question
|
|
415
|
-
held back for a later lap is a bug. One lap is the target; a second is
|
|
416
|
-
the exception
|
|
417
|
-
- Raise first, narrow second: only once every concern above the bar is
|
|
418
|
-
raised into \`## Open Questions\`, walk that same set once more and
|
|
419
|
-
answer the ones a confident default settles, moving each into
|
|
420
|
-
\`## Answered Questions\` with its answer. This pass narrows what's
|
|
421
|
-
already raised — it is never license to raise less, and skipping the
|
|
422
|
-
raise to answer straight through is the same bug as holding a
|
|
423
|
-
question back for a later lap
|
|
424
|
-
- Treat each of these as strong evidence a gap is real: it's a genuine
|
|
425
|
-
fork with divergent outcomes (user-visible for product, materially
|
|
426
|
-
different builds for technical); the diff and history don't already
|
|
427
|
-
settle it (treat an explicit \`.gtd/TODO.md\` directive, a committed
|
|
428
|
-
hand-edit, or — on a loop-back lap — the human's own review-round
|
|
429
|
-
edit/note as settled); or it would be expensive to undo once
|
|
430
|
-
packages are written
|
|
431
|
-
- Where no gap in shared understanding exists, decide it yourself:
|
|
432
|
-
record it under \`## Answered Questions\` as
|
|
433
|
-
\`### <the point phrased as a question>\`
|
|
434
|
-
plus a one-line rationale in prose, no checkboxes. That heading
|
|
435
|
-
always comes last, after every other \`##\` section
|
|
436
|
-
- Above the bar, write it under \`## Open Questions\` — always the first
|
|
437
|
-
\`##\` section — as \`### <question>\` plus a body plus a checkbox list.
|
|
438
|
-
The count of concrete options is your call, taken from how many
|
|
439
|
-
genuinely distinct answers the fork has — never padded to a fixed
|
|
440
|
-
number. A floor still applies: at least two real options plus the
|
|
441
|
-
free-text slot, or the question is not a fork
|
|
442
|
-
- The heading is followed by one or two lines of body naming the fork
|
|
443
|
-
and what actually differs, THEN the option list. The body frames the
|
|
444
|
-
fork; each option's own content carries that option's impacts — they
|
|
445
|
-
never repeat each other. A body that lists the options back is
|
|
446
|
-
padding, and an option line that restates the fork is padding too
|
|
447
|
-
- Each option is one short line plus its own impacts nested under it as
|
|
448
|
-
a bullet list: one to four bullets, free-form — what it costs, what
|
|
449
|
-
it buys, how it differs from the others. Write the dimensions this
|
|
450
|
-
fork actually has and skip the ones it does not; no \`Costs:\`/\`Buys:\`/
|
|
451
|
-
\`Differs:\` labels, no fixed bullet count. The free-text slot is last,
|
|
452
|
-
mandatory, identified by position rather than by label:
|
|
453
|
-
|
|
454
|
-
### <the question>
|
|
455
|
-
|
|
456
|
-
<one or two lines: what the fork is, what actually differs>
|
|
457
|
-
|
|
458
|
-
- [ ] <first option — a short line>
|
|
459
|
-
- <impact>
|
|
460
|
-
- <impact>
|
|
461
|
-
- [ ] <second option — a short line>
|
|
462
|
-
- <impact>
|
|
463
|
-
- [ ] <third option — a short line>
|
|
464
|
-
- <impact>
|
|
465
|
-
- <impact>
|
|
466
|
-
- <impact>
|
|
467
|
-
- [ ] _your answer_
|
|
468
|
-
|
|
469
|
-
- Never tick a box yourself — the human ticks exactly one per question`;
|
|
470
|
-
const questionBarReturn = `- This lap continues the same goal as the first: close the gap
|
|
471
|
-
between what the human wants and what gets built. Folding in
|
|
472
|
-
answers and deciding what's left is that same goal continued, not
|
|
473
|
-
a different job
|
|
474
|
-
- The human answered — a ticked box, a free-text answer in place of
|
|
475
|
-
\`_your answer_\`, a deleted question, or a deleted \`## Open Questions\`
|
|
476
|
-
section are all answers. Fold each resolved answer into its concern's
|
|
477
|
-
prose, then move the question into \`## Answered Questions\` as
|
|
478
|
-
\`### <question>\` with the resolved answer in plain prose — no
|
|
479
|
-
checkboxes
|
|
480
|
-
- \`## Answered Questions\` is always the last \`##\` section — a moved
|
|
481
|
-
question lands there, never wherever \`## Open Questions\` used to sit
|
|
482
|
-
- Never re-raise a deleted question. A settled \`## Answered Questions\`
|
|
483
|
-
entry stays settled, except one carrying a human footnote on its
|
|
484
|
-
\`### \` heading: that is the only entry that may change. Fold the note
|
|
485
|
-
in, then either rewrite the recorded answer in place (still under
|
|
486
|
-
\`## Answered Questions\`) or, when the note leaves the point genuinely
|
|
487
|
-
open, move it back to \`## Open Questions\` as a fresh fork with
|
|
488
|
-
options and the \`_your answer_\` slot. A note that only asks a
|
|
489
|
-
question follows the footnote rule: one \`- A:\` reply, answer unchanged
|
|
490
|
-
- An answer may earn a follow-up: if it opens a genuinely new fork above
|
|
491
|
-
the bar — one the answer itself created — raise it as a fresh \`##
|
|
492
|
-
Open Questions\` entry on this same lap. Never restate a question
|
|
493
|
-
already asked, and never treat this as licence to re-open a question
|
|
494
|
-
already settled under \`## Answered Questions\` that carries no
|
|
495
|
-
footnote on its heading
|
|
496
|
-
- Recognise a silent lap from \`## Open Questions\` still present with
|
|
497
|
-
nothing ticked and nothing else changed — the human's way of saying
|
|
498
|
-
the gap is already closed. That lap ends the questions, whatever the
|
|
499
|
-
goal says: decide every remaining question yourself, move each to
|
|
500
|
-
\`## Answered Questions\` with a one-line rationale, raise nothing new,
|
|
501
|
-
and leave no \`## Open Questions\` section behind`;
|
|
502
|
-
const fixFeedbackPrompt = `- The only state file this turn writes is \`.gtd/FEEDBACK.md\` — no other
|
|
503
|
-
files for notes or output
|
|
504
|
-
- Read \`.gtd/FEEDBACK.md\` (the failing test output) and fix the code so
|
|
505
|
-
the suite passes
|
|
506
|
-
- When \`.gtd/ESCALATION.md\` is present, it is a human-reviewed analysis
|
|
507
|
-
of why earlier attempts failed — read it and treat it as the primary
|
|
508
|
-
instruction for this turn. Never edit or delete it yourself, even once
|
|
509
|
-
you believe you've resolved it: only a genuinely green check retires
|
|
510
|
-
it, so an attempt that turns out to be wrong still leaves the next
|
|
511
|
-
turn's instruction in place. Its absence is the ordinary case: an
|
|
512
|
-
unremarkable red round with nothing to escalate yet
|
|
513
|
-
- Delete \`.gtd/FEEDBACK.md\` either way — once the suite passes, or once
|
|
514
|
-
you have established the feedback was wrong; leaving it in place is
|
|
515
|
-
never the right end state, the next check writes its own`;
|
|
516
|
-
const footnoteRules = `- Leave a footnote anywhere: mark the exact spot with \`[^name]\` (any
|
|
517
|
-
name, no whitespace or \`]\`), then define it below as \`[^name]:
|
|
518
|
-
explain what you mean\` — indent continuation lines by 4 spaces or
|
|
519
|
-
more. (A newly seeded definition starts with an empty body — a
|
|
520
|
-
definition whose body is still empty or whitespace-only is flagged
|
|
521
|
-
as unfilled, so write your own words there; words left under-indented
|
|
522
|
-
are flagged separately, naming the indent as the fix)
|
|
523
|
-
- A footnote is a comment on that exact spot — the hunk, line, or
|
|
524
|
-
paragraph it marks — never a whole-file remark
|
|
525
|
-
- \`name\` is yours to pick; only a definition's name must be unique in
|
|
526
|
-
this document — the same name may mark more than one spot
|
|
527
|
-
- To start a conversation, open the definition with a list: \`[^name]:\`
|
|
528
|
-
then an indented \`- H: <your question>\`. The agent answers with
|
|
529
|
-
\`- A: <reply>\`. Reply with a new \`- H:\` entry to continue; to
|
|
530
|
-
close the thread, reply with a conclusion (the agent folds it in and
|
|
531
|
-
deletes the thread) or delete the thread yourself. Entries alternate
|
|
532
|
-
\`H:\`/\`A:\` and the agent never starts a thread
|
|
533
|
-
- While a thread's last entry is the agent's, it is open and moving on
|
|
534
|
-
is refused: reply with a conclusion, or delete it
|
|
535
|
-
- The same conversation works as \`// H: <question>\` comment lines in a
|
|
536
|
-
file changed in this process (\`#\`, \`--\` or \`;\` by language); the phone UI does not show them,
|
|
537
|
-
and \`gtd check --open-threads\` lists the open ones`;
|
|
538
|
-
const footnoteFoldIn = `- A footnote (\`[^name]\` plus its \`[^name]:\` definition) is a comment on
|
|
539
|
-
its exact anchor — fold it in as a mandatory concern described
|
|
540
|
-
against that anchor's hunk or paragraph, never flattened into a
|
|
541
|
-
whole-file remark
|
|
542
|
-
- A one-shot footnote needs no reply: fold it in and DELETE it in this
|
|
543
|
-
same turn — marker and definition together. Never re-read a footnote
|
|
544
|
-
already acted on. A one-shot footnote that asks a question becomes a
|
|
545
|
-
thread instead: rewrite its definition as \`- H: <its body>\` then
|
|
546
|
-
\`- A: <your reply>\`, keeping the marker
|
|
547
|
-
- A thread is a definition whose body is a list of \`- H:\`/\`- A:\`
|
|
548
|
-
entries. Handle each by its last entry:
|
|
549
|
-
- last is \`- H:\` and asks something — reply with exactly one
|
|
550
|
-
\`- A: <reply>\` entry and change nothing else for it
|
|
551
|
-
- last concludes or advises — fold it in like a one-shot footnote,
|
|
552
|
-
then delete marker and definition
|
|
553
|
-
- last is \`- A:\` — it waits on the human; leave it alone
|
|
554
|
-
- Never start a thread yourself, and never leave a thread whose last
|
|
555
|
-
entry is \`- H:\` — a turn that does is refused`;
|
|
326
|
+
/** Run `check` again while `pending()` holds — a run then does not count — and resolve the first run that does. */
|
|
327
|
+
const untilSettled = async (check, pending) => {
|
|
328
|
+
for (;;) {
|
|
329
|
+
const green = await check();
|
|
330
|
+
if (!pending()) return green;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
const STAMP = /\n<!-- gtd check [0-9a-f]+ -->\n?$/;
|
|
334
|
+
const stripStamp = (text) => text.replace(STAMP, "\n");
|
|
335
|
+
/**
|
|
336
|
+
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
337
|
+
* first once `fixCap` fixes are spent, or `sameFailure` calls the failure
|
|
338
|
+
* identical to the previous round's.
|
|
339
|
+
*/
|
|
340
|
+
const healthy$1 = async (fix, health) => {
|
|
341
|
+
let fixes = health.fixesSoFar ?? 0;
|
|
342
|
+
let previous;
|
|
343
|
+
for (;;) {
|
|
344
|
+
if (await health.check()) {
|
|
345
|
+
health.escalations.rounds = 0;
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const current = stripStamp(read(health.report) ?? "");
|
|
349
|
+
const stuck = previous !== void 0 && await health.sameFailure(previous, current);
|
|
350
|
+
previous = current;
|
|
351
|
+
if (stuck || fixes >= health.fixCap) {
|
|
352
|
+
await escalation$1(health.escalations, health.escalation);
|
|
353
|
+
fixes = 0;
|
|
354
|
+
}
|
|
355
|
+
fixes++;
|
|
356
|
+
await fix();
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
/** Hold the process in scope `name` until `passes` resolves `true`, resting at `blocked` between tries. */
|
|
360
|
+
const gate$1 = (name, passes, blocked) => scope(name, async () => {
|
|
361
|
+
while (!await passes()) await blocked();
|
|
362
|
+
});
|
|
556
363
|
//#endregion
|
|
557
|
-
//#region src/
|
|
558
|
-
/**
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
364
|
+
//#region src/workflow-kit/quality.ts
|
|
365
|
+
/** Split a comma-separated lens list: trimmed, blanks dropped. */
|
|
366
|
+
const lensesOf = (value) => value.split(",").map((lens) => lens.trim()).filter((lens) => lens.length > 0);
|
|
367
|
+
/** One `<scope>.<lens>` entry per lens — the per-lens part of a skills or access export. */
|
|
368
|
+
const lensScopes = (scope, lenses, entry) => Object.fromEntries(lenses.map((lens) => [`${scope}.${lens}`, entry(lens)]));
|
|
369
|
+
/** One `review` turn per lens, each appending its findings to `findings`; resolves `"findings"` when that file has any. */
|
|
370
|
+
const qualityLap$1 = async (lenses, review, findings) => {
|
|
371
|
+
for (const lens of lenses) await review(lens);
|
|
372
|
+
return (read(findings) ?? "").length > 0 ? "findings" : "clean";
|
|
373
|
+
};
|
|
374
|
+
/** Resolves `true` once `findings` is gone, `false` when `cap` turns escalated instead. */
|
|
375
|
+
const fixFindings = async (f) => {
|
|
376
|
+
for (let turns = 0; turns < f.cap; turns++) {
|
|
377
|
+
await f.fix();
|
|
378
|
+
if (read(f.findings) === void 0) return true;
|
|
379
|
+
}
|
|
380
|
+
await f.escalate();
|
|
381
|
+
return false;
|
|
562
382
|
};
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
383
|
+
//#endregion
|
|
384
|
+
//#region src/workflow-kit/scenarios.ts
|
|
385
|
+
const DOC_STRING = /^(\s*)("""|```)/;
|
|
386
|
+
/**
|
|
387
|
+
* Gherkin wording only: lines trimmed; blank, `#` comment and `@` tag lines
|
|
388
|
+
* dropped — except inside a doc string, whose content is test input: kept
|
|
389
|
+
* whole, indented relative to its opening delimiter (as Gherkin reads it).
|
|
390
|
+
*/
|
|
391
|
+
const scenarioText = (source) => {
|
|
392
|
+
const out = [];
|
|
393
|
+
let doc;
|
|
394
|
+
for (const line of source.split("\n")) {
|
|
395
|
+
const delimiter = DOC_STRING.exec(line);
|
|
396
|
+
if (doc === void 0 && delimiter !== null) {
|
|
397
|
+
doc = {
|
|
398
|
+
indent: delimiter[1].length,
|
|
399
|
+
fence: delimiter[2]
|
|
400
|
+
};
|
|
401
|
+
out.push(line.trim());
|
|
402
|
+
} else if (doc !== void 0 && line.trim().startsWith(doc.fence)) {
|
|
403
|
+
doc = void 0;
|
|
404
|
+
out.push(line.trim());
|
|
405
|
+
} else if (doc !== void 0) {
|
|
406
|
+
const lead = line.length - line.trimStart().length;
|
|
407
|
+
out.push(line.slice(Math.min(lead, doc.indent)).trimEnd());
|
|
408
|
+
} else {
|
|
409
|
+
const trimmed = line.trim();
|
|
410
|
+
if (trimmed !== "" && !trimmed.startsWith("#") && !trimmed.startsWith("@")) out.push(trimmed);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return out.join("\n");
|
|
568
414
|
};
|
|
569
|
-
const
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
To start one, make ANY change — a hand-edit to real code, a scratch
|
|
573
|
-
note, anything at all. .gtd/TODO.md is a good default
|
|
574
|
-
place to start sketching. gtd treats it as a SKETCH, not finished
|
|
575
|
-
work: the very next beat unwinds it out of your working tree (its
|
|
576
|
-
intent survives in history, for the triage phase to read), then
|
|
577
|
-
checks the test baseline is green, then triages the reverted diff
|
|
578
|
-
into ordered, classified concerns: product questions, then
|
|
579
|
-
technical questions, then one package per concern, built and
|
|
580
|
-
reviewed in parallel.
|
|
581
|
-
|
|
582
|
-
What each change does next (then run \`gtd land\`):
|
|
583
|
-
- **Start** — make any change — gtd unwinds it out of your working tree (its intent survives in history) before checking the test baseline is green, then triages the reverted diff into ordered, classified concerns and resolves product questions, then technical ones, before building each concern's package (**unwind** -> **start-gate.check**).
|
|
584
|
-
`;
|
|
585
|
-
const unwindFailedMessage = () => `gtd could not revert your sketch out of the working tree.
|
|
586
|
-
\`.gtd/FEEDBACK.md\` holds the error.
|
|
587
|
-
|
|
588
|
-
Undo the sketch by hand (its intent survives in history either
|
|
589
|
-
way), then continue — gtd will not start work on a tree that still
|
|
590
|
-
carries it.
|
|
591
|
-
|
|
592
|
-
What each change does next (then run \`gtd land\`):
|
|
593
|
-
- **Continue** — having undone the sketch by hand, check the test baseline is green and start triage (**start-gate.check**).
|
|
594
|
-
`;
|
|
595
|
-
const startGateBlockedMessage = () => `The test baseline is red — gtd will not start new work on a broken suite.
|
|
596
|
-
\`.gtd/FEEDBACK.md\` holds the failing output.
|
|
597
|
-
|
|
598
|
-
What each change does next (then run \`gtd land\`):
|
|
599
|
-
- **Retry check** — edit the code and/or \`.gtd/FEEDBACK.md\` to fix the failing tests (**start-gate.check**). To repair the baseline as its own separate reviewed commit instead, abandon this start and run \`gtd --workflow fix\` from a clean \`idle\`.
|
|
600
|
-
`;
|
|
601
|
-
const reviewGateBlockedMessage = () => `The test baseline is red — gtd will not start a review on a broken suite.
|
|
602
|
-
\`.gtd/FEEDBACK.md\` holds the failing output.
|
|
603
|
-
|
|
604
|
-
What each change does next (then run \`gtd land\`):
|
|
605
|
-
- **Retry check** — edit the code and/or \`.gtd/FEEDBACK.md\` to fix the failing tests (**review-gate.check**).
|
|
606
|
-
`;
|
|
607
|
-
/** A bullet naming every code thread waiting on the agent; empty when none do. */
|
|
608
|
-
const codeThreadReplies = () => {
|
|
609
|
-
const waiting = codeThreads().filter((t) => t.waitingOn === "agent");
|
|
610
|
-
if (waiting.length === 0) return "";
|
|
611
|
-
return `
|
|
612
|
-
- Code threads waiting on you (a comment run opening with \`H:\` in a changed
|
|
613
|
-
file) — append exactly one \`A:\` comment line, same token and
|
|
614
|
-
indentation, directly below the thread's last line; or fold a
|
|
615
|
-
concluded thread in and delete its lines:
|
|
616
|
-
${waiting.map((t) => ` - ${t.path}:${t.line}: ${t.first}`).join("\n")}`;
|
|
617
|
-
};
|
|
618
|
-
const designTriagePrompt = (base) => `${styleBlock}
|
|
619
|
-
|
|
620
|
-
${styleFormatContract}
|
|
621
|
-
|
|
622
|
-
${stateFileRules}
|
|
623
|
-
${footnoteFoldIn}${codeThreadReplies()}
|
|
624
|
-
- The only state file this turn touches is \`.gtd/REQUIREMENTS.md\`
|
|
625
|
-
— no other files for notes or output
|
|
626
|
-
- \`.gtd/TODO.md\` is the likely home of the sketch that started
|
|
627
|
-
this process — the human's input, folded into the concerns
|
|
628
|
-
below like any other part of the start diff; never a state
|
|
629
|
-
file to preserve, never gtd bookkeeping to ignore
|
|
630
|
-
- This process started at commit \`${start()}\`,
|
|
631
|
-
reverted out of the tree by \`unwind\` right after landing, so
|
|
632
|
-
\`git diff ${start()}\` is now empty — its content
|
|
633
|
-
survives only in history. Find the entry commit yourself:
|
|
634
|
-
\`git rev-list --ancestry-path ${start()}..HEAD | tail -1\`
|
|
635
|
-
(the process's first turn, before any baseline-repair
|
|
636
|
-
commits), then \`git show\` it — a hand-edit, a scratch note,
|
|
637
|
-
or both
|
|
638
|
-
- Whichever lap this is, leave \`.gtd/REQUIREMENTS.md\`
|
|
639
|
-
uncommitted and finish once it reflects this lap's own work
|
|
640
|
-
|
|
641
|
-
## First lap
|
|
642
|
-
|
|
643
|
-
\`.gtd/REQUIREMENTS.md\` does not exist yet (or holds whatever the
|
|
644
|
-
human's change left there). Build it from that start diff into an
|
|
645
|
-
ordered list of concerns, each classified below.
|
|
646
|
-
|
|
647
|
-
### Classify each concern
|
|
648
|
-
|
|
649
|
-
Classify each as PRODUCT (user-facing/requirements) or TECHNICAL
|
|
650
|
-
(implementation).
|
|
651
|
-
|
|
652
|
-
${questionBar}
|
|
653
|
-
|
|
654
|
-
Raise questions only for product concerns here — technical ones
|
|
655
|
-
wait for the next phase. When every concern is TECHNICAL, write
|
|
656
|
-
no \`## Open Questions\` section. STRICT: answer it yourself only
|
|
657
|
-
when the product default is unmistakable; a wrong intent costs a
|
|
658
|
-
whole rebuild lap.
|
|
659
|
-
|
|
660
|
-
### Fold in the sketch
|
|
661
|
-
|
|
662
|
-
- Fold everything the entry commit added under the concern it
|
|
663
|
-
belongs to — a scratch note and a real code edit are both
|
|
664
|
-
just a sketch to finish, never work to preserve; \`unwind\`
|
|
665
|
-
already reverted both, so there is nothing left to delete
|
|
666
|
-
|
|
667
|
-
## Return lap
|
|
668
|
-
|
|
669
|
-
${questionBarReturn}
|
|
670
|
-
## Review loop-back
|
|
671
|
-
|
|
672
|
-
- \`.gtd/REQUIREMENTS.md\` already holds concerns with no
|
|
673
|
-
ticked-but-unfolded answer waiting — a completed REVIEW round
|
|
674
|
-
put this here, not a question you asked. Develop those
|
|
675
|
-
concerns further with whatever the round raised; never
|
|
676
|
-
rediscover or regroup them cold, the way the first lap does
|
|
677
|
-
- The human's review-round edit was reverted the same way the
|
|
678
|
-
entry commit was — read it from history:
|
|
679
|
-
\`git show ${base}\`. (On the first lap that hash
|
|
680
|
-
is the process's own diff base, \`${start()}\` — this
|
|
681
|
-
branch doesn't apply)
|
|
682
|
-
- Every decision under \`## Answered Questions\` stays settled, except
|
|
683
|
-
one carrying a human footnote on its \`### \` heading — the human's
|
|
684
|
-
review-round edit is where such notes arrive; fold the note in and
|
|
685
|
-
rewrite the answer or move it back to \`## Open Questions\`. A genuinely open PRODUCT point may still
|
|
686
|
-
raise a fresh \`## Open Questions\` entry: the product gate sits
|
|
687
|
-
on this path exactly as on the first lap
|
|
688
|
-
- Before grouping anything, run \`${env.testCommand}\`
|
|
689
|
-
yourself — a loop-back runs no green-baseline gate, so the
|
|
690
|
-
tree may already be red (reverting the edit can undo a fix it
|
|
691
|
-
made). If red, make that breakage the first concern, ahead of
|
|
692
|
-
everything else — every later concern's green-on-its-own
|
|
693
|
-
property assumes the suite started green
|
|
694
|
-
`;
|
|
695
|
-
const designSystem = () => `${designPersona}
|
|
696
|
-
|
|
697
|
-
${agentConduct}`;
|
|
698
|
-
const designGateAnswerMessage = () => `This gate stops on every process, even with no open question, so
|
|
699
|
-
you can discuss \`.gtd/REQUIREMENTS.md\`; changing nothing and re-running
|
|
700
|
-
accepts it as written — unless a thread is open (see below).
|
|
701
|
-
|
|
702
|
-
\`.gtd/REQUIREMENTS.md\` holds the concerns under
|
|
703
|
-
development. Each open question under \`## Open Questions\` offers
|
|
704
|
-
a few options plus a \`- [ ] _your answer_\` slot. Tick exactly one
|
|
705
|
-
box (\`- [x]\`) per question; for your own answer, replace
|
|
706
|
-
\`_your answer_\` with your text and tick that line. A round that
|
|
707
|
-
only leaves notes or thread replies needs no tick. Stepping is
|
|
708
|
-
refused while a question is unanswered, unless the round adds a
|
|
709
|
-
thread for the agent to answer.
|
|
710
|
-
|
|
711
|
-
Moving on is refused while any thread is open — its last entry is
|
|
712
|
-
the agent's. Reply with a conclusion, or delete the thread. A reply
|
|
713
|
-
round returns to this same gate so you can read the agent's answer.
|
|
714
|
-
|
|
715
|
-
You can also leave a footnote alongside an answer:
|
|
716
|
-
|
|
717
|
-
${footnoteRules}
|
|
718
|
-
What each change does next (then run \`gtd land\`):
|
|
719
|
-
- **Accept as-is** — change nothing and re-run; the plan stands as written. Refused while a thread is open.
|
|
720
|
-
- **Revise answers** — tick exactly one option per open question (replace \`_your answer_\` for your own) to send it back for the agent to fold your answers in, or delete a question to skip it.
|
|
721
|
-
- **Discuss** — start or continue a thread (\`- H:\`); the agent replies and this gate stops again.
|
|
722
|
-
`;
|
|
723
|
-
const architectureAuthorPrompt = () => `${styleBlock}
|
|
724
|
-
|
|
725
|
-
${styleFormatContract}
|
|
726
|
-
|
|
727
|
-
${stateFileRules}
|
|
728
|
-
${footnoteFoldIn}${codeThreadReplies()}
|
|
729
|
-
- The only state files this turn touches are
|
|
730
|
-
\`.gtd/ARCHITECTURE.md\` (write it) and \`.gtd/REQUIREMENTS.md\`
|
|
731
|
-
(delete once folded in) — no other files for notes or output
|
|
732
|
-
- You do not resume the design conversation — a separate
|
|
733
|
-
machine, its own memory, a cold read every time. Read
|
|
734
|
-
\`.gtd/REQUIREMENTS.md\` (the settled, ordered, classified
|
|
735
|
-
concerns) in full; treat every decision there, PRODUCT or
|
|
736
|
-
TECHNICAL alike, as settled — never re-open it
|
|
737
|
-
- Cold means no memory of triage's own back-and-forth, not no
|
|
738
|
-
git access. This process started at commit
|
|
739
|
-
\`${start()}\`. Find the first turn yourself: run
|
|
740
|
-
\`git rev-list --ancestry-path ${start()}..HEAD | tail -1\`
|
|
741
|
-
then \`git show\` it to see what started this process — a
|
|
742
|
-
hand-edit, a scratch note, or both
|
|
743
|
-
- Once \`.gtd/ARCHITECTURE.md\` is written, delete
|
|
744
|
-
\`.gtd/REQUIREMENTS.md\` — folded in, it must not linger. Leave
|
|
745
|
-
everything uncommitted and finish
|
|
746
|
-
|
|
747
|
-
## First lap
|
|
748
|
-
|
|
749
|
-
- Develop \`.gtd/ARCHITECTURE.md\` from those concerns: for each,
|
|
750
|
-
in order, work out the *how*, building on the settled *what*
|
|
751
|
-
- You now know the *how*, so you know each concern's file footprint
|
|
752
|
-
— list each concern's primary paths. Merge concerns whose
|
|
753
|
-
footprints center on the same files into one, unless the later
|
|
754
|
-
one only consumes an interface the earlier one creates (keeps a
|
|
755
|
-
build-on-top sequence from collapsing into one blob). This
|
|
756
|
-
authority is to merge only, never to split — the whole problem
|
|
757
|
-
is over-granularity
|
|
758
|
-
- Record every merge under \`## Merged Concerns\`,
|
|
759
|
-
carrying both merged requirements verbatim so each requirement
|
|
760
|
-
stays traceable to its tests
|
|
761
|
-
- A merge raises no open question and stops for no
|
|
762
|
-
human — do not route it to \`architecture.gate\` for a veto; the
|
|
763
|
-
human sees it when reviewing the plan
|
|
764
|
-
- Prefer fewer, larger packages — the smallest independently
|
|
765
|
-
valuable change, not the smallest change that compiles
|
|
766
|
-
- Every open point here is TECHNICAL — triage already resolved
|
|
767
|
-
the product ones, one phase earlier. PERMISSIVE: answer it
|
|
768
|
-
yourself unless you genuinely cannot defend a default; a wrong
|
|
769
|
-
technical call is still caught by the full run and the quality lap
|
|
770
|
-
- The narrow exception: a simplification that drops something
|
|
771
|
-
\`.gtd/REQUIREMENTS.md\` mentions is an open question, not a
|
|
772
|
-
silent default
|
|
773
|
-
|
|
774
|
-
- \`.gtd/ARCHITECTURE.md\` is the acceptance spec the human signs
|
|
775
|
-
off. It carries these sections, in this order:
|
|
776
|
-
1. \`## Open Questions\` (qa format, first, only when present)
|
|
777
|
-
2. \`## Interfaces\` — new and changed signatures/contracts
|
|
778
|
-
only, as fenced TypeScript (or the repository's language)
|
|
779
|
-
3. \`## Call Stacks\` — seam level, entry point → module
|
|
780
|
-
boundary; one line per hop, never per line of code
|
|
781
|
-
4. \`## E2E Scenarios\` — Gherkin in fenced \`gherkin\` blocks
|
|
782
|
-
(whatever e2e framework the repository uses), each preceded
|
|
783
|
-
by a line \`- e2e: <path>\`. With no user-visible
|
|
784
|
-
behaviour change, the literal line \`No e2e change.\` plus a
|
|
785
|
-
one-line reason — never empty or missing
|
|
786
|
-
5. \`## Unit Tests\` — one \`### <interface>\` per entry of
|
|
787
|
-
\`## Interfaces\`, each test a line
|
|
788
|
-
\`- unit: <path> — <behaviour> (covers <concern>)\`;
|
|
789
|
-
never against an internal helper. A requirement no test can
|
|
790
|
-
cover (docs, README, deletions, config) goes under a
|
|
791
|
-
\`### Chores\` heading in this section as
|
|
792
|
-
\`- chore: <path> — <what> (covers <concern>)\`
|
|
793
|
-
6. \`## Merged Concerns\` — kept, when any merge happened
|
|
794
|
-
7. \`## Answered Questions\` — last
|
|
795
|
-
- Coverage rule: every concern in \`.gtd/REQUIREMENTS.md\` appears
|
|
796
|
-
in at least one \`(covers …)\` tail, as a test or a chore. Each
|
|
797
|
-
test entry carries its file path and level (unit / e2e)
|
|
798
|
-
|
|
799
|
-
${questionBar}
|
|
800
|
-
## Return lap
|
|
801
|
-
|
|
802
|
-
${questionBarReturn}`;
|
|
803
|
-
const architectSystem = () => `${architectPersona}
|
|
804
|
-
|
|
805
|
-
${agentConduct}`;
|
|
806
|
-
const architectureDecomposePrompt = () => `${styleBlock}
|
|
807
|
-
|
|
808
|
-
${stateFileRules}
|
|
809
|
-
- The only state files this turn touches are the package files
|
|
810
|
-
under \`.gtd/packages/\` — no other files for notes or output.
|
|
811
|
-
Never delete \`.gtd/ARCHITECTURE.md\`: the build tail's full run
|
|
812
|
-
still needs it, and package files may reference it
|
|
813
|
-
- Work from \`.gtd/ARCHITECTURE.md\` if you wrote it earlier this
|
|
814
|
-
conversation, otherwise read it (the converged technical
|
|
815
|
-
plan). It already lists the concerns, interfaces, e2e scenarios
|
|
816
|
-
and unit tests — this turn groups them into packages. A
|
|
817
|
-
\`## Merged Concerns\` heading there records merges, never a
|
|
818
|
-
concern of its own: write no package file for it
|
|
819
|
-
- A package is a consecutive run of \`## Unit Tests\` entries that go
|
|
820
|
-
green together on the fast suite, at most ~8 unit-test entries
|
|
821
|
-
each. Keep the merge rule: concerns whose footprints centre on the
|
|
822
|
-
same files stay in one package unless the later one only consumes
|
|
823
|
-
an interface the earlier one creates. Prefer fewer, larger
|
|
824
|
-
packages. Order them so each stays green on the fast suite —
|
|
825
|
-
interface-introducing packages first
|
|
826
|
-
- Write the packages under \`.gtd/packages/\`, each carrying its
|
|
827
|
-
requirement(s) — both, independently, if merged — its tasks, and
|
|
828
|
-
each task's acceptance criteria as \`- [ ]\` checkboxes and
|
|
829
|
-
relevant paths:
|
|
830
|
-
- \`.gtd/packages/00-e2e-scenarios.md\` — package 0: it writes
|
|
831
|
-
\`## E2E Scenarios\` as tests in the repository's own e2e
|
|
832
|
-
framework and leaves them red. Write no such file when
|
|
833
|
-
\`## E2E Scenarios\` says "No e2e change."
|
|
834
|
-
- \`01-…\` onward — the unit-test packages. A requirement no test
|
|
835
|
-
covers (docs, README, deletions, config) becomes a chore
|
|
836
|
-
package, or a task of one; chore packages declare no tests
|
|
837
|
-
- the last numbered package is the wiring package: CLI entry,
|
|
838
|
-
workflow composition, config. A red e2e after it is a bug, not
|
|
839
|
-
missing work
|
|
840
|
-
- Every package with tests has a \`## Tests\` section, one line per
|
|
841
|
-
declared test, copied from the architecture entries:
|
|
842
|
-
\`- unit: \\\`<path>\\\`\` or \`- e2e: \\\`<path>\\\`\`. Chore packages have
|
|
843
|
-
none
|
|
844
|
-
- Leave everything uncommitted and finish
|
|
845
|
-
`;
|
|
846
|
-
const architectureGateAnswerMessage = () => `This gate stops on every process, even with no open question, so
|
|
847
|
-
you can discuss \`.gtd/ARCHITECTURE.md\`; changing nothing and re-running
|
|
848
|
-
accepts it as written — unless a thread is open (see below).
|
|
849
|
-
|
|
850
|
-
\`.gtd/ARCHITECTURE.md\` holds the technical plan under
|
|
851
|
-
development. Each open question under \`## Open Questions\` offers
|
|
852
|
-
a few options plus a \`- [ ] _your answer_\` slot. Tick exactly one
|
|
853
|
-
box (\`- [x]\`) per question; for your own answer, replace
|
|
854
|
-
\`_your answer_\` with your text and tick that line. A round that
|
|
855
|
-
only leaves notes or thread replies needs no tick. Stepping is
|
|
856
|
-
refused while a question is unanswered, unless the round adds a
|
|
857
|
-
thread for the agent to answer.
|
|
858
|
-
|
|
859
|
-
Moving on is refused while any thread is open — its last entry is
|
|
860
|
-
the agent's. Reply with a conclusion, or delete the thread. A reply
|
|
861
|
-
round returns to this same gate so you can read the agent's answer.
|
|
862
|
-
|
|
863
|
-
You can also leave a footnote alongside an answer:
|
|
864
|
-
|
|
865
|
-
${footnoteRules}
|
|
866
|
-
What each change does next (then run \`gtd land\`):
|
|
867
|
-
- **Accept as-is** — change nothing and re-run; the plan stands as written. Refused while a thread is open.
|
|
868
|
-
- **Revise answers** — tick exactly one option per open question (replace \`_your answer_\` for your own) to send it back for the agent to fold your answers in, or delete a question to skip it.
|
|
869
|
-
- **Discuss** — start or continue a thread (\`- H:\`); the agent replies and this gate stops again.
|
|
870
|
-
`;
|
|
871
|
-
const packagesItemBuildingPrompt = (pkg) => `${stateFileRules}
|
|
872
|
-
- The only state file this turn may write is \`.gtd/SATISFIED.md\`;
|
|
873
|
-
never delete the package file (the declared-tests guard reads it
|
|
874
|
-
after you)
|
|
875
|
-
- The package to implement is \`${pkg}\`
|
|
876
|
-
- First check its acceptance criteria against the current tree —
|
|
877
|
-
an earlier fix turn may already satisfy them. If **every**
|
|
878
|
-
criterion is met, implement nothing: write \`.gtd/SATISFIED.md\`
|
|
879
|
-
with each criterion's concrete evidence (commit, file, or
|
|
880
|
-
symbol), change nothing else, and finish. Otherwise implement
|
|
881
|
-
normally and skip that file
|
|
882
|
-
- Write the tests the package declares under \`## Tests\` first,
|
|
883
|
-
then the code that makes them pass; the turn is refused unless
|
|
884
|
-
every declared test is in your diff
|
|
885
|
-
- Package 0 (\`.gtd/packages/00-e2e-scenarios.md\`) writes the scenarios verbatim
|
|
886
|
-
from \`## E2E Scenarios\` of \`.gtd/ARCHITECTURE.md\`, with only
|
|
887
|
-
step code that typechecks, and leaves them red — no
|
|
888
|
-
implementation
|
|
889
|
-
- Implement every task the package describes, no more, no less,
|
|
890
|
-
fanning independent ones out to parallel subagents where your
|
|
891
|
-
harness supports it; leave other package files untouched
|
|
892
|
-
- Leave the package file in place, everything uncommitted, then
|
|
893
|
-
finish your turn
|
|
894
|
-
`;
|
|
895
|
-
const builderSystem = () => `${builderPersona}
|
|
896
|
-
|
|
897
|
-
${agentConduct}`;
|
|
898
|
-
const packagesItemFixSuitePrompt = () => `${stateFileRules}
|
|
899
|
-
${fixFeedbackPrompt}
|
|
900
|
-
- If the only way to green the suite is another package's work,
|
|
901
|
-
make the smallest change that gets there — that package can
|
|
902
|
-
then legitimately report itself already satisfied later
|
|
903
|
-
- Leave everything uncommitted and finish your turn — do not commit
|
|
904
|
-
`;
|
|
905
|
-
const healthJudgeMessage = () => `The check is still red, and this isn't the first round —
|
|
906
|
-
\`.gtd/FEEDBACK.md\` holds this round's output, and the previous round's
|
|
907
|
-
is in history. Run \`gtd judge answer\` and pipe a
|
|
908
|
-
verdict — identical, new-failure, or progress — or land (with or
|
|
909
|
-
without an edit of your own) to accept the conservative default
|
|
910
|
-
(retry the fix) with no verdict recorded.
|
|
911
|
-
`;
|
|
912
|
-
const healthDescribePrompt = () => `${stateFileRules}
|
|
913
|
-
- The only state file this turn writes is \`.gtd/ESCALATION.md\`
|
|
914
|
-
- Read \`.gtd/FEEDBACK.md\` (this round's failing check output),
|
|
915
|
-
earlier rounds' from history (\`git log -p -- .gtd/FEEDBACK.md\` —
|
|
916
|
-
this may be the first round, with no prior round to compare), and
|
|
917
|
-
the code your own earlier attempts touched
|
|
918
|
-
- Write \`.gtd/ESCALATION.md\`: what is failing, why the previous
|
|
919
|
-
attempts did not resolve it, and concrete suggested approaches to
|
|
920
|
-
solve it
|
|
921
|
-
- Never fix the code yourself — this turn only writes the document
|
|
922
|
-
`;
|
|
923
|
-
const escalationSystem = () => `${escalationPersona}
|
|
924
|
-
|
|
925
|
-
${agentConduct}`;
|
|
926
|
-
const healthStopMessage = () => `The agent could not get the check to pass after repeated attempts,
|
|
927
|
-
and has written \`.gtd/ESCALATION.md\`: what is failing, why the
|
|
928
|
-
previous attempts didn't resolve it, and suggested approaches.
|
|
929
|
-
|
|
930
|
-
Edit it — narrow it, redirect it, add what you know — or land it
|
|
931
|
-
untouched to hand it to the next fix turn as-is.
|
|
932
|
-
`;
|
|
933
|
-
const healthExhaustedMessage = () => `Escalation attempts are exhausted — this is the second round, and
|
|
934
|
-
the check is still red. \`.gtd/ESCALATION.md\` holds the last
|
|
935
|
-
unresolved analysis, and \`.gtd/FEEDBACK.md\` the last failing
|
|
936
|
-
output.
|
|
937
|
-
|
|
938
|
-
Edit \`.gtd/ESCALATION.md\` with fresh instructions for the next fix
|
|
939
|
-
turn, or land untouched to give it one more attempt at the same
|
|
940
|
-
analysis.
|
|
941
|
-
`;
|
|
942
|
-
const buildContextPrompt = (built) => {
|
|
943
|
-
const list = (paths) => paths.length === 0 ? " (none)" : paths.map((path) => ` - ${path}`).join("\n");
|
|
944
|
-
return `- Read \`.gtd/ARCHITECTURE.md\` — the plan every package was built from
|
|
945
|
-
- A red scenario that is NEW in this plan likely means missing wiring
|
|
946
|
-
between packages; new scenarios:
|
|
947
|
-
${list(built.scenarios.added)}
|
|
948
|
-
- A red CHANGED or existing scenario or test is a regression a package
|
|
949
|
-
introduced; scenarios this plan changed:
|
|
950
|
-
${list(built.scenarios.changed)}
|
|
951
|
-
- Each package's commits, to bisect a break:
|
|
952
|
-
${built.ranges.map((r) => ` git log --oneline ${r.from}..${r.to} # ${r.pkg}`).join("\n")}
|
|
953
|
-
`;
|
|
954
|
-
};
|
|
955
|
-
const buildFixPrompt = (built) => `${stateFileRules}
|
|
956
|
-
${fixFeedbackPrompt}
|
|
957
|
-
${built === void 0 ? "" : buildContextPrompt(built)}- Leave everything uncommitted — do not commit
|
|
958
|
-
`;
|
|
959
|
-
const finisherSystem = () => `${finisherPersona}
|
|
960
|
-
|
|
961
|
-
${agentConduct}`;
|
|
962
|
-
const buildFixQualityPrompt = () => `${stateFileRules}
|
|
963
|
-
- Read \`.gtd/QUALITY.md\` — one \`## \` chunk per finding a quality
|
|
964
|
-
dimension wrote. Merge duplicate findings across dimensions
|
|
965
|
-
FIRST, then fix every finding in every chunk, blocking or not
|
|
966
|
-
- When findings conflict, missing test signal beats line count —
|
|
967
|
-
a test is never deleted to satisfy a simplification finding
|
|
968
|
-
- Delete \`.gtd/QUALITY.md\` once every finding is resolved
|
|
969
|
-
- Leave everything else uncommitted and finish your turn
|
|
970
|
-
`;
|
|
971
|
-
/** Brief for a lens the workflow defines itself; a lens with none is just a skill of that name. */
|
|
972
|
-
const correctnessBrief = `- Trace partial-failure and retry paths: what a step that fails
|
|
973
|
-
after saving an id leaves behind, and whether the retry resumes
|
|
974
|
-
it or starts over
|
|
975
|
-
- Check validation done before an external side effect — its
|
|
976
|
-
format, not just its presence
|
|
977
|
-
- Check invariants that parallel write paths share: every path
|
|
978
|
-
that writes the same data must enforce what the main path
|
|
979
|
-
enforces
|
|
980
|
-
- New mock behaviour without a contract test against the live
|
|
981
|
-
behaviour (mock/live drift) is a finding`;
|
|
982
|
-
const conventionsBrief = `- Read every \`AGENTS.md\` and \`CLAUDE.md\` in the repository root
|
|
983
|
-
and in each touched file's directory ancestry, end to end, plus
|
|
984
|
-
every file they pull in by \`@path\`
|
|
985
|
-
- Every violation of them in the change is a finding — quote the
|
|
986
|
-
rule it breaks`;
|
|
987
|
-
const specChallengeBrief = `- Find this process's planning documents in history:
|
|
988
|
-
\`git log <start>..HEAD\` (\`<start>\` is the commit above) over the steering directory
|
|
989
|
-
(\`.gtd/\`), then \`git show\` the last version of the
|
|
990
|
-
requirements, architecture and package files
|
|
991
|
-
- Flag a spec decision that conflicts with a system invariant — an
|
|
992
|
-
existing test, a documented constraint, or a data invariant
|
|
993
|
-
other code relies on — naming the decision and the invariant
|
|
994
|
-
- With no planning documents in history, write nothing`;
|
|
995
|
-
const buildQualityReviewingPrompt = (lens, brief) => `${stateFileRules}
|
|
996
|
-
- The only state file this turn writes is \`.gtd/QUALITY.md\` — no
|
|
997
|
-
other files for notes or output
|
|
998
|
-
- Review the whole assembled change, from \`${start()}\`
|
|
999
|
-
to the working tree, through this ONE quality lens only —
|
|
1000
|
-
\`${lens}\`
|
|
1001
|
-
- Trace, do not skim: follow the order of external calls against
|
|
1002
|
-
the resume/retry logic. On a large change, read the touched code
|
|
1003
|
-
paths, not just the diff hunks
|
|
1004
|
-
- A test counts as coverage only if it would fail with the guarded
|
|
1005
|
-
behaviour removed — decide that by reasoning, never by running a
|
|
1006
|
-
mutation-testing tool. A test that pins a bug is a finding, not
|
|
1007
|
-
praise
|
|
1008
|
-
- APPEND every finding you have, blocking or not, as a \`## \`
|
|
1009
|
-
chunk to \`.gtd/QUALITY.md\` — never overwrite what an earlier
|
|
1010
|
-
dimension already wrote there
|
|
1011
|
-
- Write nothing only when this lens found nothing at all — then a
|
|
1012
|
-
clean turn IS this dimension's approval
|
|
1013
|
-
${brief ? `
|
|
1014
|
-
This lens's brief:
|
|
1015
|
-
${brief}
|
|
1016
|
-
|
|
1017
|
-
` : ""}- Touch no other state file, and leave everything uncommitted
|
|
1018
|
-
`;
|
|
1019
|
-
const reviewerSystem = () => `${reviewerPersona}
|
|
1020
|
-
|
|
1021
|
-
${agentConduct}`;
|
|
1022
|
-
const buildReviewAwaitReviewMessage = (base) => `\`.gtd/REVIEW.md\` holds the review record for the process — one
|
|
1023
|
-
\`- [ ]\` checkbox per reviewable item, grouped into chunks. Tick a box
|
|
1024
|
-
(\`- [x]\`) as you review each hunk; ticking only records that you've read
|
|
1025
|
-
it, it is not sign-off. Ticks are read-progress only: landing clears
|
|
1026
|
-
every box back to \`- [ ]\` on disk and nothing records which hunks
|
|
1027
|
-
you read — there is no persisted trail of it.
|
|
1028
|
-
|
|
1029
|
-
Review the diff yourself, with whatever tool you like — gtd checks
|
|
1030
|
-
nothing out and touches no ref; \`gtd base\` prints this same hash any
|
|
1031
|
-
time you need it again. The range runs from the review base to the
|
|
1032
|
-
working tree:
|
|
1033
|
-
|
|
1034
|
-
git diff ${base}
|
|
1035
|
-
When you've been through the whole diff, run \`gtd land\`:
|
|
1036
|
-
|
|
1037
|
-
- **Sign off** — leave no comment — no note in
|
|
1038
|
-
\`.gtd/REVIEW.md\`, no code edit — to close the process,
|
|
1039
|
-
whatever the boxes say. Every turn commit stays on the
|
|
1040
|
-
branch; run \`gtd summary\` afterward for a closing-message
|
|
1041
|
-
prompt.
|
|
1042
|
-
- **Ask a question** — start a thread (\`- H: <question>\`) on a
|
|
1043
|
-
footnote. The agent answers inside \`.gtd/REVIEW.md\` and the process
|
|
1044
|
-
rests at this same gate again — no revert, no development lap. A
|
|
1045
|
-
round that also leaves notes or edits folds those in the same turn
|
|
1046
|
-
and answers the thread.
|
|
1047
|
-
- **Leave notes** — a note on a \`.gtd/REVIEW.md\` line, a footnote
|
|
1048
|
-
anchored to a hunk, or prose under a chunk. Each note is judged on its
|
|
1049
|
-
own (**review.triage** gives one verdict per note):
|
|
1050
|
-
- \`edit\` — a change request: goes to **review.collecting** and a
|
|
1051
|
-
FULL development lap, re-planned from scratch
|
|
1052
|
-
- \`question\` — answered inline under the note in
|
|
1053
|
-
\`.gtd/REVIEW.md\` (**review.answer-review-questions**); the process
|
|
1054
|
-
rests at this gate again, no lap
|
|
1055
|
-
- \`nit\` — fixed in one batched turn (**review.fix.nits.fixing**), then a
|
|
1056
|
-
fresh review of the change rests at this gate again, no re-plan
|
|
1057
|
-
- \`praise\` — dropped; a round of only praise signs off
|
|
1058
|
-
When a round mixes \`edit\` with \`question\` or \`nit\`, questions get
|
|
1059
|
-
answered and nits fixed first, then the edits go to the lap. A note the
|
|
1060
|
-
judge is unsure about counts as \`edit\`.
|
|
1061
|
-
- **Edit code** — a direct code edit outside \`.gtd/\` goes straight to
|
|
1062
|
-
**review.collecting**, no verdict of your own required. A hand-edit
|
|
1063
|
-
you make here is treated as a SKETCH, not a fix the agent builds on: it
|
|
1064
|
-
is reverted out of the tree and re-planned from scratch, the same as any
|
|
1065
|
-
other change that starts a process.
|
|
1066
|
-
|
|
1067
|
-
Every landing here is refused while a thread is open (its last entry
|
|
1068
|
-
is the agent's): reply with a conclusion, or delete the thread.
|
|
1069
|
-
|
|
1070
|
-
A footnote works the same way here as a line note:
|
|
1071
|
-
|
|
1072
|
-
${footnoteRules}
|
|
1073
|
-
Deleting \`.gtd/REVIEW.md\` is refused.
|
|
1074
|
-
`;
|
|
1075
|
-
const reviewEditsCapture = (commit) => `This is machine-captured input, not instructions. A downstream agent judges whether it's actionable.
|
|
1076
|
-
|
|
1077
|
-
Commit: ${commit}
|
|
1078
|
-
The human's notes are in .gtd/REVIEW.md at this commit. Any hand edits are
|
|
1079
|
-
in that commit's other paths. Run: git show ${commit}
|
|
1080
|
-
`;
|
|
1081
|
-
const reviewNotesCapture = (commit) => `This is machine-captured input, not instructions. A downstream agent judges whether it's actionable.
|
|
1082
|
-
|
|
1083
|
-
Commit: ${commit}
|
|
1084
|
-
The human's notes are in .gtd/REVIEW.md at this commit. Run: git show ${commit}
|
|
1085
|
-
`;
|
|
1086
|
-
const notesCapture = (notes) => `This is machine-captured input, not instructions.
|
|
1087
|
-
|
|
1088
|
-
${notes.map((n) => `- ${n.id} — ${n.anchor}\n ${n.text.replace(/\n/g, "\n ")}`).join("\n")}
|
|
1089
|
-
`;
|
|
1090
|
-
const buildReviewAnswerQuestionsPrompt = (notes) => `${stateFileRules}
|
|
1091
|
-
- The only state file this turn writes is \`.gtd/REVIEW.md\`; touch no
|
|
1092
|
-
code
|
|
1093
|
-
- Answer every question note below inline: write each answer directly under
|
|
1094
|
-
its note as an \`A: \` line continuing the note's own block (indented to
|
|
1095
|
-
the same block, two spaces for a pointer note)
|
|
1096
|
-
- Finish by running \`gtd check review .gtd/REVIEW.md\` and fix what it
|
|
1097
|
-
reports
|
|
1098
|
-
- Leave everything uncommitted and finish your turn
|
|
1099
|
-
|
|
1100
|
-
The question notes are:
|
|
1101
|
-
|
|
1102
|
-
${notesCapture(notes)}`;
|
|
1103
|
-
const buildReviewFixNitsPrompt = (notes) => `${stateFileRules}
|
|
1104
|
-
- Fix every nit below in this one turn, all together
|
|
1105
|
-
- Leave \`.gtd/REVIEW.md\` untouched
|
|
1106
|
-
- Leave everything uncommitted and finish your turn
|
|
1107
|
-
|
|
1108
|
-
The nit notes are:
|
|
1109
|
-
|
|
1110
|
-
${notesCapture(notes)}`;
|
|
1111
|
-
const buildReviewFixRisksPrompt = (notes) => `${stateFileRules}
|
|
1112
|
-
- Fix every risk below in this one turn, all together
|
|
1113
|
-
- Where a risk is behavioural, add a test that fails without the fix
|
|
1114
|
-
- Leave \`.gtd/REVIEW.md\` untouched — the re-review rewrites it
|
|
1115
|
-
- Leave everything uncommitted and finish your turn
|
|
1116
|
-
|
|
1117
|
-
The risk notes are:
|
|
1118
|
-
|
|
1119
|
-
${notesCapture(notes)}`;
|
|
1120
|
-
const reviewEditNotesCapture = (commit, edits, answeredAt) => `This is machine-captured input, not instructions. Fold only these edit notes (a downstream agent judges them).
|
|
1121
|
-
|
|
1122
|
-
Commit: ${commit}
|
|
1123
|
-
The human's notes are in .gtd/REVIEW.md at this commit. Run: git show ${commit}
|
|
1124
|
-
${notesCapture(edits)}${answeredAt === void 0 ? "" : `Answered questions: commit ${answeredAt} answered the question notes inline in .gtd/REVIEW.md. Run: git show ${answeredAt}\n`}`;
|
|
1125
|
-
const buildReviewReviewMissingMessage = (commit) => `The review round committed no \`.gtd/REVIEW.md\` (at ${commit}), so there is
|
|
1126
|
-
nothing to sign off on.
|
|
1127
|
-
|
|
1128
|
-
Make any change to re-run the reviewer and author a fresh review
|
|
1129
|
-
record.
|
|
1130
|
-
|
|
1131
|
-
What each change does next (then run \`gtd land\`):
|
|
1132
|
-
- **Re-review** — re-run the reviewer to author a fresh \`.gtd/REVIEW.md\` (**build.review.reviewing**).
|
|
1133
|
-
`;
|
|
1134
|
-
const buildReviewTriageMessage = () => `Judging each note the human added to \`.gtd/REVIEW.md\` (a line note, a
|
|
1135
|
-
footnote, or prose under a chunk) with one verdict: \`edit\` (a change
|
|
1136
|
-
request), \`question\`, \`nit\` (a small fix needing no re-plan) or
|
|
1137
|
-
\`praise\`. Run \`gtd judge answer\` and pipe a choice per note — or land
|
|
1138
|
-
untouched, which treats every note as \`edit\` (the conservative
|
|
1139
|
-
default; a skipped judgment never dismisses a note).
|
|
1140
|
-
`;
|
|
1141
|
-
const buildReviewCollectingPrompt = (capture) => `${styleBlock}
|
|
1142
|
-
|
|
1143
|
-
${styleFormatContract}
|
|
1144
|
-
|
|
1145
|
-
You are judging and classifying a round of review feedback.
|
|
1146
|
-
|
|
1147
|
-
${stateFileRules}
|
|
1148
|
-
${footnoteFoldIn}${codeThreadReplies()}
|
|
1149
|
-
- This turn writes \`.gtd/REQUIREMENTS.md\` (the folded concerns) and
|
|
1150
|
-
replies inside \`.gtd/REVIEW.md\` (thread replies only; the file stays
|
|
1151
|
-
in the tree) — you classify, you do not build
|
|
1152
|
-
- Fold every concluded thread, ticked answer and hand-edit — and each
|
|
1153
|
-
\`edit\` note the capture lists, no other note — into
|
|
1154
|
-
\`.gtd/REQUIREMENTS.md\` and delete the folded threads from
|
|
1155
|
-
\`.gtd/REVIEW.md\`; append one \`- A:\` reply to each thread
|
|
1156
|
-
whose last entry is a \`- H:\` question
|
|
1157
|
-
- When the capture names an answering commit, carry each answered
|
|
1158
|
-
question into \`.gtd/REQUIREMENTS.md\`'s \`## Answered Questions\` as
|
|
1159
|
-
\`### <the note>\` plus its answer — the lap replaces \`.gtd/REVIEW.md\`
|
|
1160
|
-
- Nit notes were already fixed: never re-raise a fixed nit as a concern
|
|
1161
|
-
- Finish by running \`gtd check review .gtd/REVIEW.md\` and fix
|
|
1162
|
-
what it reports
|
|
1163
|
-
|
|
1164
|
-
The raw review material is:
|
|
1165
|
-
|
|
1166
|
-
${capture}
|
|
1167
|
-
It names a commit. Work from what you already reviewed if you
|
|
1168
|
-
wrote today's review earlier this conversation; otherwise read
|
|
1169
|
-
that commit's diff yourself first.
|
|
1170
|
-
|
|
1171
|
-
The round is actionable if any of these hold:
|
|
1172
|
-
|
|
1173
|
-
- The human left a note on \`.gtd/REVIEW.md\`. A note is a mandatory
|
|
1174
|
-
concern below
|
|
1175
|
-
- The human added a code comment this round. A comment run whose
|
|
1176
|
-
first line starts \`H:\` is a THREAD: when its last entry is an
|
|
1177
|
-
\`H:\` question, write exactly one \`A:\` comment line, same token
|
|
1178
|
-
and indentation, directly below it — no fold-in; a concluded
|
|
1179
|
-
thread is folded into the requirements and its comment lines
|
|
1180
|
-
deleted. Any other comment, even a plain-prose one, is a one-shot
|
|
1181
|
-
concern — describe it, and note the comment line itself is
|
|
1182
|
-
transient: it must not survive the lap that satisfies it
|
|
1183
|
-
- The human hand-edited non-comment code this round — no longer
|
|
1184
|
-
a committed intent to build on, but a sketch like the entry
|
|
1185
|
-
commit's own diff. Describe what it was reaching for; expect
|
|
1186
|
-
the next lap to re-derive it from scratch, never call it final
|
|
1187
|
-
|
|
1188
|
-
Not actionable only when none of the above holds — nothing but an
|
|
1189
|
-
approving remark, no code edit, no substantive note. Never invent
|
|
1190
|
-
actionability, and never dismiss a real note or edit as approval.
|
|
1191
|
-
|
|
1192
|
-
- If actionable: write \`.gtd/REQUIREMENTS.md\` with an ordered
|
|
1193
|
-
list of concerns, each PRODUCT or TECHNICAL — the shape
|
|
1194
|
-
\`design.triage\` builds, one \`## <heading>\` per concern in
|
|
1195
|
-
build order. Fold every note, comment, and hand-edit in under
|
|
1196
|
-
its concern. Raise no open questions here — \`design.triage\`
|
|
1197
|
-
owns that later. Then finish
|
|
1198
|
-
- If not: finish without writing anything — changing nothing is
|
|
1199
|
-
the sign-off
|
|
1200
|
-
`;
|
|
1201
|
-
const buildReviewReviewingPrompt = (base, carry) => `${styleBlock}
|
|
1202
|
-
|
|
1203
|
-
${styleFormatContract}
|
|
1204
|
-
|
|
1205
|
-
${stateFileRules}
|
|
1206
|
-
- The only state file this turn touches is \`.gtd/REVIEW.md\` —
|
|
1207
|
-
no other files for notes or output
|
|
1208
|
-
|
|
1209
|
-
Write \`.gtd/REVIEW.md\` in this exact format, to help a human
|
|
1210
|
-
review the changes:
|
|
1211
|
-
|
|
1212
|
-
- First non-blank line: \`# Review: ${head$1().slice(0, 7)}\`
|
|
1213
|
-
- Somewhere in the document: \`<!-- base: ${base} -->\`
|
|
1214
|
-
- At least one \`## <Chunk Title>\` heading grouping hunks
|
|
1215
|
-
semantically (same feature/refactor/fix, across files), each
|
|
1216
|
-
with a short explanation of what changed and why, then one
|
|
1217
|
-
pointer per hunk (\`./\`-relative path, mandatory \`#start-end\`;
|
|
1218
|
-
checkboxes are for the human, not you). A pointer into a NEW file
|
|
1219
|
-
gets a range covering only the region the note is about — never
|
|
1220
|
-
the whole file. Put the note's opening line right on the
|
|
1221
|
-
pointer's line:
|
|
1222
|
-
|
|
1223
|
-
- [ ] ./path/to/file.ts#42-70 — what this hunk does
|
|
1224
|
-
|
|
1225
|
-
Continue a longer note below the pointer, indented exactly two spaces
|
|
1226
|
-
— never four or more, which reads as a code block and never
|
|
1227
|
-
reflows:
|
|
1228
|
-
|
|
1229
|
-
- [ ] ./path/to/file.ts#42-70 — what this hunk does
|
|
1230
|
-
and here is more detail, continued below it
|
|
1231
|
-
|
|
1232
|
-
Open a hunk's note with \`Risk:\` only for a concrete defect
|
|
1233
|
-
the change introduces, never a style remark — each such note is
|
|
1234
|
-
fixed automatically before the human sees the review. On the
|
|
1235
|
-
re-review after a fix, describe what the fix changed under the
|
|
1236
|
-
hunk it touched, and re-mark only a risk the fix did not resolve:
|
|
1237
|
-
|
|
1238
|
-
- [ ] ./path/to/file.ts#42-70 — Risk: what is wrong
|
|
1239
|
-
|
|
1240
|
-
A note sitting entirely on the line(s) beneath the pointer is
|
|
1241
|
-
also valid. Either way, the note must never start with a bare \`./path\` token
|
|
1242
|
-
— that parses as a second pointer, not a note
|
|
1243
|
-
No diff is given — read the changes yourself. The range runs
|
|
1244
|
-
from \`${base}\` to the working tree (committed turns
|
|
1245
|
-
plus anything pending); on a feedback round that's the previous
|
|
1246
|
-
review's boundary, so it covers only what's new.
|
|
1247
|
-
|
|
1248
|
-
${carry === void 0 ? "" : `Carry-over: commit \`${carry}\` answered questions inline in the previous
|
|
1249
|
-
review (each \`A: \` line under the note it answers). Copy each answered
|
|
1250
|
-
note and its \`A: \` answer, verbatim, under the matching hunk of this fresh
|
|
1251
|
-
review. Run: git show ${carry}
|
|
1252
|
-
|
|
1253
|
-
`}Leave \`.gtd/REVIEW.md\` uncommitted and finish.
|
|
1254
|
-
`;
|
|
1255
|
-
/** `gtd summary`'s prompt: printed cold, no session identity, no diff inlined. */
|
|
1256
|
-
const summaryPrompt = (it) => {
|
|
1257
|
-
const human = it.humanCommits.length > 0 ? `The human contributed at these commits,
|
|
1258
|
-
oldest to newest:
|
|
1259
|
-
${it.humanCommits.map((c) => `- \`${c.hash}\` (entering \`${c.state}\`)\n`).join("")}
|
|
1260
|
-
` : `The human left no comment or edit this process —
|
|
1261
|
-
every commit is machine-authored.
|
|
1262
|
-
`;
|
|
1263
|
-
return `${styleBlock}
|
|
1264
|
-
|
|
1265
|
-
- Write the closing message for the process HEAD closes or sits inside —
|
|
1266
|
-
for a squash, an amend, or a PR body. Starting cold: read every
|
|
1267
|
-
decision out of the commits below, not assumed context
|
|
1268
|
-
- Cover the motivation, the decisions, the trade-offs, and the
|
|
1269
|
-
high-level architectural changes — never which files changed; \`git
|
|
1270
|
-
diff --stat\` is for that, not this message
|
|
1271
|
-
- If the range removes or renames a documented config key, CLI flag, or
|
|
1272
|
-
workflow state, OR makes a previously-accepted config value now fail to
|
|
1273
|
-
load (a stricter validation, an escape a value must now spell differently)
|
|
1274
|
-
— a public surface, not an internal one, either way — add a literal
|
|
1275
|
-
\`BREAKING CHANGE:\` footer naming what breaks and, for the second case,
|
|
1276
|
-
the exact escape or rewrite a committed config needs. A \`!\` on the
|
|
1277
|
-
type prefix alone (\`feat!:\`/\`refactor!:\`) is NOT enough: this repo's
|
|
1278
|
-
own commit-analyzer has missed that marker before, cutting no release
|
|
1279
|
-
until a follow-up commit carried the literal footer instead
|
|
1280
|
-
|
|
1281
|
-
The process's entry commit is \`${it.entryCommit}\`. ${human}Inspect the range: \`git log ${it.processBase}..${it.processTip}\`
|
|
1282
|
-
and \`git diff ${it.processBase} ${it.processTip}\` — exactly what
|
|
1283
|
-
a squash or PR body should describe.
|
|
1284
|
-
|
|
1285
|
-
Token cost: ${it.processCost}
|
|
1286
|
-
${it.processCostByModel.map((m) => `- ${m.model}: ${m.cost}\n`).join("")}
|
|
1287
|
-
Print the closing message and stop — this writes nothing itself.
|
|
1288
|
-
`;
|
|
1289
|
-
};
|
|
1290
|
-
const scenarioWordingMessage = (at, drifted) => `The e2e scenario wording was frozen when package 0 landed, and the last
|
|
1291
|
-
turn changed it. Removed lines are marked \`-\`, added ones \`+\`:
|
|
1292
|
-
|
|
1293
|
-
${drifted.map(({ path, lines }) => `${path}\n${lines.join("\n")}`).join("\n\n")}
|
|
1294
|
-
|
|
1295
|
-
To accept the change, land untouched.
|
|
1296
|
-
|
|
1297
|
-
To reject it, run
|
|
1298
|
-
|
|
1299
|
-
git checkout ${at} -- ${drifted.map(({ path }) => quote(path)).join(" ")}
|
|
1300
|
-
|
|
1301
|
-
and land. Restoring only some of the files is a partial accept: every file
|
|
1302
|
-
still differing from the frozen wording is accepted.
|
|
1303
|
-
`;
|
|
1304
|
-
//#endregion
|
|
1305
|
-
//#region src/workflows/steps.ts
|
|
1306
|
-
const FEEDBACK = ".gtd/FEEDBACK.md";
|
|
1307
|
-
const ESCALATION = ".gtd/ESCALATION.md";
|
|
1308
|
-
const REQUIREMENTS = ".gtd/REQUIREMENTS.md";
|
|
1309
|
-
const ARCHITECTURE = ".gtd/ARCHITECTURE.md";
|
|
1310
|
-
const REVIEW = ".gtd/REVIEW.md";
|
|
1311
|
-
const QUALITY = ".gtd/QUALITY.md";
|
|
1312
|
-
const planner = () => env.plannerModel ?? "";
|
|
1313
|
-
const coder = () => env.coderModel ?? "";
|
|
1314
|
-
/** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
|
|
1315
|
-
const triage = (base) => agentWithSkills("triage", designTriagePrompt(base), {
|
|
1316
|
-
label: "Triaging the change",
|
|
1317
|
-
file: REQUIREMENTS,
|
|
1318
|
-
mode: "qa",
|
|
1319
|
-
model: planner(),
|
|
1320
|
-
system: designSystem()
|
|
1321
|
-
});
|
|
1322
|
-
const author = () => agentWithSkills("author", architectureAuthorPrompt(), {
|
|
1323
|
-
label: "Refining the technical plan",
|
|
1324
|
-
file: ARCHITECTURE,
|
|
1325
|
-
mode: "qa",
|
|
1326
|
-
model: planner(),
|
|
1327
|
-
system: architectSystem()
|
|
1328
|
-
});
|
|
1329
|
-
const decompose = () => agentWithSkills("decompose.decomposing", architectureDecomposePrompt(), {
|
|
1330
|
-
label: "Decomposing into packages",
|
|
1331
|
-
model: planner(),
|
|
1332
|
-
system: architectSystem()
|
|
1333
|
-
});
|
|
1334
|
-
const answerProductQuestions = () => human("gate.answer", {
|
|
1335
|
-
message: designGateAnswerMessage(),
|
|
1336
|
-
label: "Awaiting your product answers",
|
|
1337
|
-
file: REQUIREMENTS,
|
|
1338
|
-
mode: "qa",
|
|
1339
|
-
acceptClean: true
|
|
1340
|
-
});
|
|
1341
|
-
const answerTechnicalQuestions = () => human("gate.answer", {
|
|
1342
|
-
message: architectureGateAnswerMessage(),
|
|
1343
|
-
label: "Awaiting your technical answers",
|
|
1344
|
-
file: ARCHITECTURE,
|
|
1345
|
-
mode: "qa",
|
|
1346
|
-
acceptClean: true
|
|
1347
|
-
});
|
|
1348
|
-
const build = (pkg) => agentWithSkills("building", packagesItemBuildingPrompt(pkg), {
|
|
1349
|
-
label: "Building",
|
|
1350
|
-
model: coder(),
|
|
1351
|
-
system: builderSystem()
|
|
1352
|
-
});
|
|
1353
|
-
const fixSuite = () => agentWithSkills("fix.suite.fixing", packagesItemFixSuitePrompt(), {
|
|
1354
|
-
label: "Fixing the check",
|
|
1355
|
-
file: FEEDBACK,
|
|
1356
|
-
model: coder(),
|
|
1357
|
-
system: builderSystem()
|
|
1358
|
-
});
|
|
1359
|
-
const fixCheck = (built) => agentWithSkills("fix", buildFixPrompt(built), {
|
|
1360
|
-
label: "Fixing the check",
|
|
1361
|
-
file: FEEDBACK,
|
|
1362
|
-
model: coder(),
|
|
1363
|
-
system: finisherSystem()
|
|
1364
|
-
});
|
|
1365
|
-
const describeEscalation = () => agentWithSkills("health.describe", healthDescribePrompt(), {
|
|
1366
|
-
label: "Describing the escalation",
|
|
1367
|
-
file: FEEDBACK,
|
|
1368
|
-
model: coder(),
|
|
1369
|
-
system: escalationSystem(),
|
|
1370
|
-
allowEmpty: true
|
|
1371
|
-
});
|
|
1372
|
-
const escalate = () => human("health.stop", {
|
|
1373
|
-
message: healthStopMessage(),
|
|
1374
|
-
label: "Escalating to a human",
|
|
1375
|
-
file: ESCALATION,
|
|
1376
|
-
acceptClean: true
|
|
1377
|
-
});
|
|
1378
|
-
const escalationExhausted = () => human("health.exhausted", {
|
|
1379
|
-
message: healthExhaustedMessage(),
|
|
1380
|
-
label: "Escalation exhausted",
|
|
1381
|
-
file: ESCALATION,
|
|
1382
|
-
acceptClean: true
|
|
1383
|
-
});
|
|
1384
|
-
/** Lenses the workflow defines itself, not bundled skills: `skills/` is not in the npm package, and a missing lens skill burns a turn silently. */
|
|
1385
|
-
const builtInLenses = {
|
|
1386
|
-
correctness: {
|
|
1387
|
-
skills: ["code-review-and-quality"],
|
|
1388
|
-
brief: correctnessBrief
|
|
1389
|
-
},
|
|
1390
|
-
conventions: {
|
|
1391
|
-
skills: [],
|
|
1392
|
-
brief: conventionsBrief
|
|
1393
|
-
},
|
|
1394
|
-
"spec-challenge": {
|
|
1395
|
-
skills: [],
|
|
1396
|
-
brief: specChallengeBrief
|
|
1397
|
-
}
|
|
1398
|
-
};
|
|
1399
|
-
/** One quality review through `lens`; each lens is its own scope, so its own conversation and skills. */
|
|
1400
|
-
const reviewQuality = (lens) => agentWithSkills(`quality.${lens}.reviewing`, buildQualityReviewingPrompt(lens, builtInLenses[lens]?.brief), {
|
|
1401
|
-
label: "Reviewing (one quality lens)",
|
|
1402
|
-
file: QUALITY,
|
|
1403
|
-
model: planner(),
|
|
1404
|
-
system: reviewerSystem(),
|
|
1405
|
-
allowEmpty: true
|
|
1406
|
-
});
|
|
1407
|
-
const fixQuality = () => agentWithSkills("fix.quality.fixing", buildFixQualityPrompt(), {
|
|
1408
|
-
label: "Fixing quality findings",
|
|
1409
|
-
file: QUALITY,
|
|
1410
|
-
model: coder(),
|
|
1411
|
-
system: finisherSystem(),
|
|
1412
|
-
allowEmpty: true
|
|
1413
|
-
});
|
|
1414
|
-
/** Write `.gtd/REVIEW.md` over everything since `base`, carrying the answers of commit `carry` when given. */
|
|
1415
|
-
const reviewing = (base, carry) => agentWithSkills("review.reviewing", buildReviewReviewingPrompt(base, carry), {
|
|
1416
|
-
label: "Reviewing",
|
|
1417
|
-
file: REVIEW,
|
|
1418
|
-
mode: "review",
|
|
1419
|
-
model: planner(),
|
|
1420
|
-
system: reviewerSystem(),
|
|
1421
|
-
base
|
|
1422
|
-
});
|
|
1423
|
-
/** Answer every `question` note inline in `.gtd/REVIEW.md`. */
|
|
1424
|
-
const answerReviewQuestions = (notes) => agentWithSkills("review.answer-review-questions", buildReviewAnswerQuestionsPrompt(notes), {
|
|
1425
|
-
label: "Answering your questions",
|
|
1426
|
-
file: REVIEW,
|
|
1427
|
-
mode: "review",
|
|
1428
|
-
model: planner(),
|
|
1429
|
-
system: reviewerSystem()
|
|
1430
|
-
});
|
|
1431
|
-
/** Fix every `nit` note in one turn. */
|
|
1432
|
-
const fixNits = (notes) => agentWithSkills("review.fix.nits.fixing", buildReviewFixNitsPrompt(notes), {
|
|
1433
|
-
label: "Fixing your nits",
|
|
1434
|
-
file: REVIEW,
|
|
1435
|
-
model: planner(),
|
|
1436
|
-
system: reviewerSystem()
|
|
1437
|
-
});
|
|
1438
|
-
/** Fix the `Risk:`-marked notes the reviewer named; an empty turn means the risk was judged false. */
|
|
1439
|
-
const fixRisks = (notes) => agentWithSkills("review.fix.risks.fixing", buildReviewFixRisksPrompt(notes), {
|
|
1440
|
-
label: "Fixing the reviewer's risks",
|
|
1441
|
-
file: REVIEW,
|
|
1442
|
-
model: planner(),
|
|
1443
|
-
system: reviewerSystem(),
|
|
1444
|
-
allowEmpty: true
|
|
1445
|
-
});
|
|
1446
|
-
const awaitReview = (base) => human("review.await-review", {
|
|
1447
|
-
message: buildReviewAwaitReviewMessage(base),
|
|
1448
|
-
label: "Awaiting your review",
|
|
1449
|
-
file: REVIEW,
|
|
1450
|
-
mode: "review",
|
|
1451
|
-
base
|
|
1452
|
-
});
|
|
1453
|
-
/** The round at commit `round` left no `.gtd/REVIEW.md`. */
|
|
1454
|
-
const reviewMissing = (round) => human("review.review-missing", {
|
|
1455
|
-
message: buildReviewReviewMissingMessage(round),
|
|
1456
|
-
label: "Nothing to review"
|
|
1457
|
-
});
|
|
1458
|
-
/** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
|
|
1459
|
-
const collecting = (capture) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture), {
|
|
1460
|
-
label: "Collecting your feedback",
|
|
1461
|
-
file: REQUIREMENTS,
|
|
1462
|
-
mode: "qa",
|
|
1463
|
-
model: planner(),
|
|
1464
|
-
system: reviewerSystem(),
|
|
1465
|
-
allowEmpty: true
|
|
1466
|
-
});
|
|
1467
|
-
//#endregion
|
|
1468
|
-
//#region src/workflows/health.ts
|
|
1469
|
-
/** Written while `fastTestCommand` is unset; its presence rests the process at the check. */
|
|
1470
|
-
const SETUP = ".gtd/SETUP.md";
|
|
1471
|
-
const SETUP_TEXT = "The bundled workflow needs the `fastTestCommand` setting: the fast suite (everything but e2e).\nSet it as a workflow option in `gtd.config.ts` or as GTD_FASTTESTCOMMAND.";
|
|
1472
|
-
/** Shell lines for a check's preamble: unset (blank after trim) setting writes `SETUP` and skips the suite. */
|
|
1473
|
-
const fastSuiteGuard = () => (env.fastTestCommand ?? "").trim() === "" ? [
|
|
1474
|
-
`mkdir -p ${quote(dirname(SETUP))}`,
|
|
1475
|
-
`printf '%s\\n' ${quote(SETUP_TEXT)} > ${quote(SETUP)}`,
|
|
1476
|
-
`printf '%s\\n' ${quote(SETUP_TEXT)} >&2`,
|
|
1477
|
-
"exit 0"
|
|
1478
|
-
] : [`rm -f ${quote(SETUP)}`];
|
|
1479
|
-
/** Fix turns a run of red checks gets before it escalates. */
|
|
1480
|
-
const FIX_CAP = 3;
|
|
1481
|
-
/** Run the suite as step `name`; resolves `true` when it passed. A failure is in `.gtd/FEEDBACK.md`. */
|
|
1482
|
-
const baseline = async (name, label = "Checking the baseline") => {
|
|
1483
|
-
for (;;) {
|
|
1484
|
-
const green = await check(name, env.testCommand ?? "", {
|
|
1485
|
-
report: FEEDBACK,
|
|
1486
|
-
label,
|
|
1487
|
-
preamble: fastSuiteGuard()
|
|
1488
|
-
});
|
|
1489
|
-
if (read(".gtd/SETUP.md") === void 0) return green;
|
|
1490
|
-
}
|
|
1491
|
-
};
|
|
1492
|
-
/**
|
|
1493
|
-
* Hand a red suite that repeated attempts could not fix to a person: an
|
|
1494
|
-
* agent writes `.gtd/ESCALATION.md`, a human narrows it. From the third round
|
|
1495
|
-
* on, the human is told the budget is spent instead.
|
|
1496
|
-
*/
|
|
1497
|
-
const escalation = async (count) => {
|
|
1498
|
-
if (count.rounds >= 2) return escalationExhausted();
|
|
1499
|
-
count.rounds++;
|
|
1500
|
-
await describeEscalation();
|
|
1501
|
-
await escalate();
|
|
1502
|
-
};
|
|
1503
|
-
const STAMP = /\n<!-- gtd check [0-9a-f]+ -->\n?$/;
|
|
1504
|
-
const stripStamp = (text) => text.replace(STAMP, "\n");
|
|
1505
|
-
/** "Is this red round the same failure as the last one?" — asked of two reports, or of nothing when one is empty. */
|
|
1506
|
-
const retryQuestion = (comparable) => comparable ? {
|
|
1507
|
-
id: "verdict",
|
|
1508
|
-
primitive: "choice",
|
|
1509
|
-
instructions: "Compare this round's failing check output (current) against the previous round's (previous), both in state. Classify the change.",
|
|
1510
|
-
criteria: "identical: the same failure restated, byte-for-byte or the same root cause — the trailing `<!-- gtd check <sha> -->` stamp is already stripped from both, but ignore it too if it ever reappears. new-failure: a materially different symptom than previous. progress: still red, but measurably closer to green (fewer failures, a later stage reached)."
|
|
1511
|
-
} : {
|
|
1512
|
-
id: "verdict",
|
|
1513
|
-
primitive: "choice",
|
|
1514
|
-
instructions: "state carries tailsNotComparable: one of the two check reports came back empty. There is nothing to compare here; answer from criteria alone. `identical` is FORBIDDEN here.",
|
|
1515
|
-
criteria: "new-failure: a materially different failure than before. progress: still red, but closer to green — the default if genuinely unsure."
|
|
1516
|
-
};
|
|
1517
|
-
const sameFailure = async (previous, current) => {
|
|
1518
|
-
const comparable = current.trim() !== "" && previous.trim() !== "";
|
|
1519
|
-
const { answers, truncated } = await judge("health.judge", {
|
|
1520
|
-
questions: [retryQuestion(comparable)],
|
|
1521
|
-
evidence: comparable ? {
|
|
1522
|
-
current,
|
|
1523
|
-
previous
|
|
1524
|
-
} : { tailsNotComparable: "true" },
|
|
1525
|
-
message: healthJudgeMessage(),
|
|
1526
|
-
label: "Judging the retry"
|
|
1527
|
-
});
|
|
1528
|
-
return comparable && truncated.length === 0 && answered(answers.verdict, "identical", numeric(vars.judgeIdenticalMinP, Infinity));
|
|
1529
|
-
};
|
|
1530
|
-
const runSuite = (options) => {
|
|
1531
|
-
const fast = options.suite === "fast";
|
|
1532
|
-
return check("health.check", (fast ? env.fastTestCommand : env.testCommand) ?? "", {
|
|
1533
|
-
report: FEEDBACK,
|
|
1534
|
-
label: "Running checks",
|
|
1535
|
-
sweepOnGreen: [ESCALATION, ...options.sweepOnGreen ?? []],
|
|
1536
|
-
...fast ? { preamble: fastSuiteGuard() } : {}
|
|
1537
|
-
});
|
|
1538
|
-
};
|
|
1539
|
-
/**
|
|
1540
|
-
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
1541
|
-
* first once `FIX_CAP` fixes are spent, or a judge calls the failure
|
|
1542
|
-
* identical to the previous round's.
|
|
1543
|
-
*/
|
|
1544
|
-
const healthy = async (fix, options = {}) => {
|
|
1545
|
-
const escalations = options.escalations ?? { rounds: 0 };
|
|
1546
|
-
let fixes = options.fixesSoFar ?? 0;
|
|
1547
|
-
let previous;
|
|
1548
|
-
for (;;) {
|
|
1549
|
-
const green = await runSuite(options);
|
|
1550
|
-
if (options.suite === "fast" && read(".gtd/SETUP.md") !== void 0) continue;
|
|
1551
|
-
if (green) {
|
|
1552
|
-
escalations.rounds = 0;
|
|
1553
|
-
return;
|
|
1554
|
-
}
|
|
1555
|
-
const current = stripStamp(read(".gtd/FEEDBACK.md") ?? "");
|
|
1556
|
-
const stuck = previous !== void 0 && await sameFailure(previous, current);
|
|
1557
|
-
previous = current;
|
|
1558
|
-
if (stuck || fixes >= 3) {
|
|
1559
|
-
await escalation(escalations);
|
|
1560
|
-
fixes = 0;
|
|
1561
|
-
}
|
|
1562
|
-
fixes++;
|
|
1563
|
-
await fix();
|
|
1564
|
-
}
|
|
1565
|
-
};
|
|
1566
|
-
/** Hold the process at `<name>.blocked` until the suite is green. */
|
|
1567
|
-
const gate = (name, message) => scope(name, async () => {
|
|
1568
|
-
while (!await baseline("check")) await human("blocked", {
|
|
1569
|
-
message,
|
|
1570
|
-
label: "Baseline is red",
|
|
1571
|
-
file: FEEDBACK
|
|
1572
|
-
});
|
|
1573
|
-
});
|
|
1574
|
-
//#endregion
|
|
1575
|
-
//#region src/workflows/scenarios.ts
|
|
1576
|
-
const DOC_STRING = /^(\s*)("""|```)/;
|
|
1577
|
-
/**
|
|
1578
|
-
* Gherkin wording only: lines trimmed; blank, `#` comment and `@` tag lines
|
|
1579
|
-
* dropped — except inside a doc string, whose content is test input: kept
|
|
1580
|
-
* whole, indented relative to its opening delimiter (as Gherkin reads it).
|
|
1581
|
-
*/
|
|
1582
|
-
const scenarioText = (source) => {
|
|
1583
|
-
const out = [];
|
|
1584
|
-
let doc;
|
|
1585
|
-
for (const line of source.split("\n")) {
|
|
1586
|
-
const delimiter = DOC_STRING.exec(line);
|
|
1587
|
-
if (doc === void 0 && delimiter !== null) {
|
|
1588
|
-
doc = {
|
|
1589
|
-
indent: delimiter[1].length,
|
|
1590
|
-
fence: delimiter[2]
|
|
1591
|
-
};
|
|
1592
|
-
out.push(line.trim());
|
|
1593
|
-
} else if (doc !== void 0 && line.trim().startsWith(doc.fence)) {
|
|
1594
|
-
doc = void 0;
|
|
1595
|
-
out.push(line.trim());
|
|
1596
|
-
} else if (doc !== void 0) {
|
|
1597
|
-
const lead = line.length - line.trimStart().length;
|
|
1598
|
-
out.push(line.slice(Math.min(lead, doc.indent)).trimEnd());
|
|
1599
|
-
} else {
|
|
1600
|
-
const trimmed = line.trim();
|
|
1601
|
-
if (trimmed !== "" && !trimmed.startsWith("#") && !trimmed.startsWith("@")) out.push(trimmed);
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
return out.join("\n");
|
|
1605
|
-
};
|
|
1606
|
-
const textOf = (path) => {
|
|
1607
|
-
const source = read(path);
|
|
1608
|
-
return source === void 0 ? void 0 : scenarioText(source);
|
|
415
|
+
const textOf = (path) => {
|
|
416
|
+
const source = read(path);
|
|
417
|
+
return source === void 0 ? void 0 : scenarioText(source);
|
|
1609
418
|
};
|
|
1610
419
|
/** Snapshot `.feature` paths at `head()`, merged over `previous`; `undefined` when nothing is frozen. */
|
|
1611
420
|
const freezeScenarios = (paths, previous) => {
|
|
@@ -1650,17 +459,16 @@ const wordingDrift = (frozen, path) => ({
|
|
|
1650
459
|
path,
|
|
1651
460
|
lines: lineDiff(lines(frozen.texts[path]), lines(textOf(path)))
|
|
1652
461
|
});
|
|
1653
|
-
/**
|
|
1654
|
-
|
|
462
|
+
/**
|
|
463
|
+
* After a turn: rest at `stop` while wording drifted from `frozen`. Whatever
|
|
464
|
+
* still differs after it is accepted and moves the snapshot; what was
|
|
465
|
+
* restored stays frozen as it was.
|
|
466
|
+
*/
|
|
467
|
+
const holdWording$1 = async (frozen, stop) => {
|
|
1655
468
|
if (frozen === void 0) return;
|
|
1656
469
|
const drifted = driftedScenarios(frozen);
|
|
1657
470
|
if (drifted.length === 0) return;
|
|
1658
|
-
await
|
|
1659
|
-
message: scenarioWordingMessage(frozen.at, drifted.map((path) => wordingDrift(frozen, path))),
|
|
1660
|
-
label: "Scenario wording changed",
|
|
1661
|
-
acceptClean: true,
|
|
1662
|
-
base: frozen.at
|
|
1663
|
-
});
|
|
471
|
+
await stop(frozen.at, drifted.map((path) => wordingDrift(frozen, path)));
|
|
1664
472
|
const accepted = driftedScenarios(frozen);
|
|
1665
473
|
if (accepted.length === 0) return;
|
|
1666
474
|
for (const path of accepted) {
|
|
@@ -1670,120 +478,6 @@ const holdWording = async (frozen) => {
|
|
|
1670
478
|
}
|
|
1671
479
|
frozen.at = head$1();
|
|
1672
480
|
};
|
|
1673
|
-
/** `turn`, then `holdWording`; `within` names the scope the gate rests in. */
|
|
1674
|
-
const guarded = (frozen, turn, within) => async () => {
|
|
1675
|
-
await turn();
|
|
1676
|
-
if (within === void 0) await holdWording(frozen);
|
|
1677
|
-
else await scope(within, () => holdWording(frozen));
|
|
1678
|
-
};
|
|
1679
|
-
//#endregion
|
|
1680
|
-
//#region src/workflows/packages.ts
|
|
1681
|
-
/** Package 0: the e2e scenarios, written red. */
|
|
1682
|
-
const SCENARIO_PACKAGE = ".gtd/packages/00-e2e-scenarios.md";
|
|
1683
|
-
const TEST_LINE = /^\s*-\s+(unit|e2e):\s+`([^`]+)`/;
|
|
1684
|
-
/** The `## Tests` entries of a package file; malformed lines are ignored. */
|
|
1685
|
-
const declaredTests = (packageText) => {
|
|
1686
|
-
const found = [];
|
|
1687
|
-
let inTests = false;
|
|
1688
|
-
for (const line of packageText.split("\n")) if (/^##\s/.test(line)) inTests = /^##\s+Tests\s*$/.test(line);
|
|
1689
|
-
else if (inTests) {
|
|
1690
|
-
const match = TEST_LINE.exec(line);
|
|
1691
|
-
if (match) found.push({
|
|
1692
|
-
level: match[1],
|
|
1693
|
-
path: match[2]
|
|
1694
|
-
});
|
|
1695
|
-
}
|
|
1696
|
-
return found;
|
|
1697
|
-
};
|
|
1698
|
-
/**
|
|
1699
|
-
* Refuse a build or fix turn unless every test `pkg` declares exists and is in the
|
|
1700
|
-
* diff since `since`. The package text is read as of `since`, so a builder
|
|
1701
|
-
* editing its own `## Tests` cannot dodge the guard; with `.gtd/SATISFIED.md`
|
|
1702
|
-
* written, existing in the tree suffices.
|
|
1703
|
-
*/
|
|
1704
|
-
const requireDeclaredTests = (pkg, since) => {
|
|
1705
|
-
const range = changesSince(since);
|
|
1706
|
-
const text = range.get(pkg)?.before ?? read(pkg) ?? "";
|
|
1707
|
-
const satisfied = read(".gtd/SATISFIED.md") !== void 0;
|
|
1708
|
-
const missing = declaredTests(text).map((test) => test.path).filter((path) => satisfied ? read(path) === void 0 : range.get(path) === void 0 || range.get(path)?.status === "deleted" || read(path) === void 0);
|
|
1709
|
-
if (missing.length === 0) return;
|
|
1710
|
-
refuse(`gtd land: declared-tests: ${pkg} declares tests that are ${satisfied ? "missing from the tree" : "not in the package's diff"} — write them first:\n${missing.map((path) => ` - ${path}`).join("\n")}`);
|
|
1711
|
-
};
|
|
1712
|
-
/** Build `pkg`, check its declared tests, keep the fast suite green, and close it out, which removes it. */
|
|
1713
|
-
const packageItem = async (pkg, frozen) => {
|
|
1714
|
-
const from = head$1();
|
|
1715
|
-
const declared = (turn) => async () => {
|
|
1716
|
-
await turn();
|
|
1717
|
-
requireDeclaredTests(pkg, from);
|
|
1718
|
-
};
|
|
1719
|
-
await guarded(frozen, declared(() => build(pkg)))();
|
|
1720
|
-
await healthy(guarded(frozen, declared(fixSuite)), { suite: "fast" });
|
|
1721
|
-
await run("closing", removeScript([pkg, ".gtd/SATISFIED.md"]), { label: "Closing out the package" });
|
|
1722
|
-
return {
|
|
1723
|
-
pkg,
|
|
1724
|
-
from,
|
|
1725
|
-
to: head$1()
|
|
1726
|
-
};
|
|
1727
|
-
};
|
|
1728
|
-
/** The first queued package — the one a package step works on. */
|
|
1729
|
-
const nextPackage = () => [...glob(".gtd/packages/*.md")].sort()[0];
|
|
1730
|
-
const outsideGtd = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
1731
|
-
/** Build every package file under `.gtd/packages/`, in name order. */
|
|
1732
|
-
const packages = (carried) => scope("packages", async () => {
|
|
1733
|
-
let frozen = carried;
|
|
1734
|
-
const ranges = [];
|
|
1735
|
-
let touched = [];
|
|
1736
|
-
for (let pkg = nextPackage(); pkg !== void 0; pkg = nextPackage()) {
|
|
1737
|
-
const range = await scope("item", () => packageItem(pkg, pkg === ".gtd/packages/00-e2e-scenarios.md" ? void 0 : frozen));
|
|
1738
|
-
ranges.push(range);
|
|
1739
|
-
if (pkg === ".gtd/packages/00-e2e-scenarios.md") {
|
|
1740
|
-
touched = changesSince(range.from).filter((c) => outsideGtd(c.path) && c.status !== "deleted");
|
|
1741
|
-
frozen = freezeScenarios(touched.filter((c) => c.path.endsWith(".feature")).map((c) => c.path), frozen);
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
const paths = (status) => touched.filter((c) => c.status === status).map((c) => c.path);
|
|
1745
|
-
return {
|
|
1746
|
-
ranges,
|
|
1747
|
-
scenarios: {
|
|
1748
|
-
added: paths("added"),
|
|
1749
|
-
changed: paths("modified")
|
|
1750
|
-
},
|
|
1751
|
-
frozen
|
|
1752
|
-
};
|
|
1753
|
-
});
|
|
1754
|
-
//#endregion
|
|
1755
|
-
//#region src/workflows/planning.ts
|
|
1756
|
-
/**
|
|
1757
|
-
* Always stop for the human. Resolves `true` when the round changed anything
|
|
1758
|
-
* (the author revises), `false` on a clean re-run. Open threads refuse every
|
|
1759
|
-
* landing; unticked questions only refuse a round with no thread for the
|
|
1760
|
-
* agent to answer.
|
|
1761
|
-
*/
|
|
1762
|
-
const noteGate = async (file, answer) => {
|
|
1763
|
-
await answer();
|
|
1764
|
-
requireThreadsClosed(file);
|
|
1765
|
-
if (changes().length === 0) return false;
|
|
1766
|
-
if (!hasThreadFor("agent", file)) requireAnswers(file);
|
|
1767
|
-
return true;
|
|
1768
|
-
};
|
|
1769
|
-
/** Triage the sketch since `base` into requirements, until no product question is open. */
|
|
1770
|
-
const design = (base) => scope("design", async () => {
|
|
1771
|
-
do {
|
|
1772
|
-
await triage(base);
|
|
1773
|
-
requireProgress(REQUIREMENTS);
|
|
1774
|
-
requireReplies(REQUIREMENTS);
|
|
1775
|
-
} while (await noteGate(REQUIREMENTS, answerProductQuestions));
|
|
1776
|
-
});
|
|
1777
|
-
/** Work out the technical plan, then decompose it into package files. */
|
|
1778
|
-
const architecture = () => scope("architecture", async () => {
|
|
1779
|
-
do {
|
|
1780
|
-
await author();
|
|
1781
|
-
requireReplies(ARCHITECTURE);
|
|
1782
|
-
} while (await noteGate(ARCHITECTURE, answerTechnicalQuestions));
|
|
1783
|
-
await decompose();
|
|
1784
|
-
if (changes(".gtd/packages/**").length === 0) refuse("gtd land: decompose: write at least one package under .gtd/packages/");
|
|
1785
|
-
if (changes(".gtd/ARCHITECTURE.md").some((c) => c.status === "deleted")) refuse("gtd land: decompose: keep .gtd/ARCHITECTURE.md — the build tail's full run needs it; it is removed only once that run is green");
|
|
1786
|
-
});
|
|
1787
481
|
//#endregion
|
|
1788
482
|
//#region node_modules/mdast-util-to-string/lib/index.js
|
|
1789
483
|
/**
|
|
@@ -17762,282 +16456,1435 @@ const commentOf = (line, tokens) => {
|
|
|
17762
16456
|
text: trimmed.slice(token.length).trim()
|
|
17763
16457
|
};
|
|
17764
16458
|
};
|
|
17765
|
-
const endsThreadRun = (texts, next) => PREFIX_RE.test(texts[0]) && texts[texts.length - 1].startsWith("A:") && !PREFIX_RE.test(next);
|
|
17766
|
-
const runsOf = (lines, tokens) => {
|
|
17767
|
-
const runs = [];
|
|
17768
|
-
let current;
|
|
17769
|
-
const close = (end) => {
|
|
17770
|
-
if (current) runs.push({
|
|
17771
|
-
start: current.start,
|
|
17772
|
-
end,
|
|
17773
|
-
texts: current.texts
|
|
17774
|
-
});
|
|
17775
|
-
current = void 0;
|
|
17776
|
-
};
|
|
17777
|
-
lines.forEach((line, i) => {
|
|
17778
|
-
const comment = commentOf(line, tokens);
|
|
17779
|
-
if (!comment || current && (current.token !== comment.token || endsThreadRun(current.texts, comment.text))) close(i - 1);
|
|
17780
|
-
if (!comment) return;
|
|
17781
|
-
if (current) current.texts.push(comment.text);
|
|
17782
|
-
else current = {
|
|
17783
|
-
start: i,
|
|
17784
|
-
token: comment.token,
|
|
17785
|
-
texts: [comment.text]
|
|
17786
|
-
};
|
|
17787
|
-
});
|
|
17788
|
-
close(lines.length - 1);
|
|
17789
|
-
return runs;
|
|
16459
|
+
const endsThreadRun = (texts, next) => PREFIX_RE.test(texts[0]) && texts[texts.length - 1].startsWith("A:") && !PREFIX_RE.test(next);
|
|
16460
|
+
const runsOf = (lines, tokens) => {
|
|
16461
|
+
const runs = [];
|
|
16462
|
+
let current;
|
|
16463
|
+
const close = (end) => {
|
|
16464
|
+
if (current) runs.push({
|
|
16465
|
+
start: current.start,
|
|
16466
|
+
end,
|
|
16467
|
+
texts: current.texts
|
|
16468
|
+
});
|
|
16469
|
+
current = void 0;
|
|
16470
|
+
};
|
|
16471
|
+
lines.forEach((line, i) => {
|
|
16472
|
+
const comment = commentOf(line, tokens);
|
|
16473
|
+
if (!comment || current && (current.token !== comment.token || endsThreadRun(current.texts, comment.text))) close(i - 1);
|
|
16474
|
+
if (!comment) return;
|
|
16475
|
+
if (current) current.texts.push(comment.text);
|
|
16476
|
+
else current = {
|
|
16477
|
+
start: i,
|
|
16478
|
+
token: comment.token,
|
|
16479
|
+
texts: [comment.text]
|
|
16480
|
+
};
|
|
16481
|
+
});
|
|
16482
|
+
close(lines.length - 1);
|
|
16483
|
+
return runs;
|
|
16484
|
+
};
|
|
16485
|
+
const isThreadRun = (run) => PREFIX_RE.test(run.texts[0]);
|
|
16486
|
+
/** `text` with every thread run removed; ordinary comments stay. */
|
|
16487
|
+
const stripCodeThreads = (path, text) => {
|
|
16488
|
+
const tokens = codeThreadTokens(path);
|
|
16489
|
+
if (tokens.length === 0) return text;
|
|
16490
|
+
const lines = text.split("\n");
|
|
16491
|
+
const drop = /* @__PURE__ */ new Set();
|
|
16492
|
+
for (const run of runsOf(lines.map((l) => l.replace(/\r$/, "")), tokens).filter(isThreadRun)) for (let i = run.start; i <= run.end; i++) drop.add(i);
|
|
16493
|
+
return lines.filter((_, i) => !drop.has(i)).join("\n");
|
|
16494
|
+
};
|
|
16495
|
+
[...(/* @__PURE__ */ new Map([["qa", qaDescriptor], ["review", reviewDescriptor]])).keys()];
|
|
16496
|
+
//#endregion
|
|
16497
|
+
//#region src/workflow-kit/review.ts
|
|
16498
|
+
const collect = async (p, capture) => {
|
|
16499
|
+
await p.collecting(capture);
|
|
16500
|
+
if (read(p.feedback) !== void 0) return "feedback";
|
|
16501
|
+
if (changes().length === 0) return "signoff";
|
|
16502
|
+
return refuse(p.refusals.unmatched);
|
|
16503
|
+
};
|
|
16504
|
+
/** A review with ticks, footnote markers and thread definitions removed: what is left is line notes. */
|
|
16505
|
+
const notesOf = (text) => text.replace(/^\[\^[^\]]+\]:[^\n]*(\n(?: {2,}|\t)[^\n]*|\n(?=\s*\n(?: {2,}|\t)))*/gm, "").replace(/\[\^[^\]]+\]/g, "").replace(/\[x\]/gi, "[ ]").replace(/\s+/g, " ").trim();
|
|
16506
|
+
/**
|
|
16507
|
+
* The human gate and its reply rounds: a question gets an answer inside
|
|
16508
|
+
* the review file and rests at the gate again, without a lap. Resolves the
|
|
16509
|
+
* commit of the last collecting turn, `"missing"` when the round left no
|
|
16510
|
+
* review, or `undefined` when no such turn ran.
|
|
16511
|
+
*/
|
|
16512
|
+
const converse = async (p, base, collectedBefore) => {
|
|
16513
|
+
let collectedAt = collectedBefore;
|
|
16514
|
+
for (;;) {
|
|
16515
|
+
await p.awaitReview(base);
|
|
16516
|
+
if (changes(p.file).some((c) => c.status === "deleted")) refuse(p.refusals.deleted);
|
|
16517
|
+
if (read(p.file) === void 0) {
|
|
16518
|
+
await p.reviewMissing(head$1());
|
|
16519
|
+
return "missing";
|
|
16520
|
+
}
|
|
16521
|
+
requireThreadsClosed(p.file);
|
|
16522
|
+
if (!hasThreadFor("agent", p.file)) return collectedAt;
|
|
16523
|
+
await p.collecting(p.captures.edits(head$1()));
|
|
16524
|
+
collectedAt = head$1();
|
|
16525
|
+
requireReplies(p.file);
|
|
16526
|
+
if (!hasThreadFor("human", p.file)) return collectedAt;
|
|
16527
|
+
}
|
|
16528
|
+
};
|
|
16529
|
+
/** Judge every note, then run each verdict's route: answers, then nit fixes, then the edits' lap. */
|
|
16530
|
+
const routeNotes = async (p, notes, r) => {
|
|
16531
|
+
const verdicts = await p.triage(notes);
|
|
16532
|
+
const of = (v) => notes.filter((n) => (verdicts.get(n.id) ?? "edit") === v);
|
|
16533
|
+
const [edits, questions, nits] = [
|
|
16534
|
+
of("edit"),
|
|
16535
|
+
of("question"),
|
|
16536
|
+
of("nit")
|
|
16537
|
+
];
|
|
16538
|
+
if (edits.length + questions.length + nits.length === 0) return r.unfolded();
|
|
16539
|
+
let answeredAt;
|
|
16540
|
+
if (questions.length > 0) {
|
|
16541
|
+
await p.answerQuestions(questions);
|
|
16542
|
+
answeredAt = head$1();
|
|
16543
|
+
}
|
|
16544
|
+
if (nits.length > 0) {
|
|
16545
|
+
await p.fixNits(nits);
|
|
16546
|
+
await p.keepGreen();
|
|
16547
|
+
}
|
|
16548
|
+
if (edits.length > 0) {
|
|
16549
|
+
await r.close();
|
|
16550
|
+
return r.outcome(await collect(p, p.captures.editNotes(r.round, edits, answeredAt)));
|
|
16551
|
+
}
|
|
16552
|
+
if (nits.length > 0) {
|
|
16553
|
+
await r.close();
|
|
16554
|
+
return {
|
|
16555
|
+
next: "rereview",
|
|
16556
|
+
carry: answeredAt
|
|
16557
|
+
};
|
|
16558
|
+
}
|
|
16559
|
+
return {
|
|
16560
|
+
next: "await",
|
|
16561
|
+
reviewed: answeredAt
|
|
16562
|
+
};
|
|
16563
|
+
};
|
|
16564
|
+
/** A code edit, not a thread-only change: lines that only add, answer or remove code threads don't count. */
|
|
16565
|
+
const isCodeEdit = (p, c) => p.isCode(c.path) && stripCodeThreads(c.path, c.before ?? "") !== stripCodeThreads(c.path, c.after ?? "");
|
|
16566
|
+
/** Whether anything but thread edits and ticks landed in the review file since `commit`. */
|
|
16567
|
+
const notedSince = (p, commit) => changesSince(commit).some((c) => c.path === p.file && notesOf(c.before ?? "") !== notesOf(read(p.file) ?? ""));
|
|
16568
|
+
/**
|
|
16569
|
+
* What the in-loop collecting turn left: `folded` when it wrote `feedback`
|
|
16570
|
+
* (those folds need their lap whatever is judged later), `settled` when only
|
|
16571
|
+
* thread edits and ticks landed after it.
|
|
16572
|
+
*/
|
|
16573
|
+
const afterCollect = (p, collectedAt) => collectedAt === "" ? {
|
|
16574
|
+
folded: false,
|
|
16575
|
+
settled: false
|
|
16576
|
+
} : settledAfter(p, collectedAt);
|
|
16577
|
+
const settledAfter = (p, collectedAt) => ({
|
|
16578
|
+
folded: collectedAt !== void 0 && read(p.feedback) !== void 0,
|
|
16579
|
+
settled: collectedAt !== void 0 && changesSince(collectedAt).every((c) => !isCodeEdit(p, c)) && !notedSince(p, collectedAt)
|
|
16580
|
+
});
|
|
16581
|
+
const finish = async (p, reviewed, collectedAt) => {
|
|
16582
|
+
const round = head$1();
|
|
16583
|
+
const since = changesSince(reviewed);
|
|
16584
|
+
const edited = since.filter((c) => isCodeEdit(p, c));
|
|
16585
|
+
const baselineText = since.get(p.file)?.before ?? "";
|
|
16586
|
+
const record = read(p.file) ?? "";
|
|
16587
|
+
const { folded, settled } = afterCollect(p, collectedAt);
|
|
16588
|
+
const noted = since.some((c) => c.path === p.file);
|
|
16589
|
+
const close = () => p.close(round);
|
|
16590
|
+
const outcome = (verdict) => verdict === "signoff" ? { verdict } : {
|
|
16591
|
+
verdict,
|
|
16592
|
+
base: round,
|
|
16593
|
+
restoreFrom: reviewed,
|
|
16594
|
+
edited
|
|
16595
|
+
};
|
|
16596
|
+
const unfolded = async () => {
|
|
16597
|
+
await close();
|
|
16598
|
+
return outcome(folded ? "feedback" : "signoff");
|
|
16599
|
+
};
|
|
16600
|
+
const collectWith = async (capture) => {
|
|
16601
|
+
await close();
|
|
16602
|
+
return outcome(await collect(p, capture));
|
|
16603
|
+
};
|
|
16604
|
+
if (settled || edited.length === 0 && !noted) return unfolded();
|
|
16605
|
+
if (edited.length > 0) return collectWith(p.captures.edits(round));
|
|
16606
|
+
const notes = reviewNotes(baselineText, record);
|
|
16607
|
+
if (notes.length === 0) return collectWith(p.captures.notes(round));
|
|
16608
|
+
return routeNotes(p, notes, {
|
|
16609
|
+
round,
|
|
16610
|
+
close,
|
|
16611
|
+
outcome,
|
|
16612
|
+
unfolded
|
|
16613
|
+
});
|
|
16614
|
+
};
|
|
16615
|
+
/** Write the review; if it marks risks, fix them, keep green, and write it again — once, so the re-review's own risks reach the human unfixed. */
|
|
16616
|
+
const reviewOnce = async (p, base, carry) => {
|
|
16617
|
+
await p.reviewing(base, carry);
|
|
16618
|
+
const risks = reviewRisks(read(p.file) ?? "");
|
|
16619
|
+
if (risks.length === 0) return;
|
|
16620
|
+
await p.fixRisks(risks);
|
|
16621
|
+
await p.keepGreen();
|
|
16622
|
+
await p.reviewing(base, carry);
|
|
16623
|
+
};
|
|
16624
|
+
/**
|
|
16625
|
+
* A reviewer writes the review over everything since `base`, a human
|
|
16626
|
+
* reviews and signs off or comments, and each note is judged: edits go to
|
|
16627
|
+
* another lap, questions are answered in place, nits fixed in one batch,
|
|
16628
|
+
* praise dropped.
|
|
16629
|
+
*/
|
|
16630
|
+
const review$2 = async (base, p) => {
|
|
16631
|
+
let carry;
|
|
16632
|
+
for (;;) {
|
|
16633
|
+
await reviewOnce(p, base, carry);
|
|
16634
|
+
carry = void 0;
|
|
16635
|
+
let reviewed = head$1();
|
|
16636
|
+
let collectedAt;
|
|
16637
|
+
for (;;) {
|
|
16638
|
+
collectedAt = await converse(p, base, collectedAt);
|
|
16639
|
+
if (collectedAt === "missing") break;
|
|
16640
|
+
const result = await finish(p, reviewed, collectedAt);
|
|
16641
|
+
if (!("next" in result)) return result;
|
|
16642
|
+
if (result.next === "rereview") {
|
|
16643
|
+
carry = result.carry;
|
|
16644
|
+
break;
|
|
16645
|
+
}
|
|
16646
|
+
reviewed = result.reviewed;
|
|
16647
|
+
}
|
|
16648
|
+
}
|
|
16649
|
+
};
|
|
16650
|
+
/**
|
|
16651
|
+
* A reviewer writes the review over everything since `base` and a human
|
|
16652
|
+
* reviews it. No note signs off and closes; any note ends the round with the
|
|
16653
|
+
* review left in place, for another workflow to read its notes — nothing is
|
|
16654
|
+
* judged or re-planned here.
|
|
16655
|
+
*/
|
|
16656
|
+
const reviewToHandOff = async (base, p) => {
|
|
16657
|
+
for (;;) {
|
|
16658
|
+
await reviewOnce(p, base, void 0);
|
|
16659
|
+
const reviewed = head$1();
|
|
16660
|
+
await p.awaitReview(base);
|
|
16661
|
+
if (changes(p.file).some((c) => c.status === "deleted")) refuse(p.deleted);
|
|
16662
|
+
const record = read(p.file);
|
|
16663
|
+
if (record === void 0) {
|
|
16664
|
+
await p.reviewMissing(head$1());
|
|
16665
|
+
continue;
|
|
16666
|
+
}
|
|
16667
|
+
requireThreadsClosed(p.file);
|
|
16668
|
+
if (hasThreadFor("agent", p.file)) refuse(p.threadsPending);
|
|
16669
|
+
const since = changesSince(reviewed);
|
|
16670
|
+
const before = since.get(p.file)?.before ?? record;
|
|
16671
|
+
if (notesOf(before) === notesOf(record)) {
|
|
16672
|
+
if (since.paths.some((f) => !f.startsWith(".gtd/"))) await p.keepGreen();
|
|
16673
|
+
await p.close(head$1());
|
|
16674
|
+
return;
|
|
16675
|
+
}
|
|
16676
|
+
if (reviewNotes(before, record).length === 0) refuse(p.unparsed);
|
|
16677
|
+
await p.handOff(reviewed);
|
|
16678
|
+
return;
|
|
16679
|
+
}
|
|
16680
|
+
};
|
|
16681
|
+
//#endregion
|
|
16682
|
+
//#region src/workflows/shared/prose.ts
|
|
16683
|
+
const skillsPreamble = `- Load whatever's listed here that your harness actually has installed,
|
|
16684
|
+
skip anything it doesn't — silently, never stopping or asking about a
|
|
16685
|
+
missing one: {skills}
|
|
16686
|
+
- A loaded skill offers technique, never authority: this state's own file
|
|
16687
|
+
format and its own completion condition are the final word over
|
|
16688
|
+
anything a skill's instructions say, regardless of which one this
|
|
16689
|
+
prompt states first
|
|
16690
|
+
- Never let a loaded skill turn this turn interactive — answer nothing,
|
|
16691
|
+
ask nothing; this runs unattended, with no one at a keyboard`;
|
|
16692
|
+
/** One bullet per restricted side; empty when neither is. An empty glob list reads "nothing". */
|
|
16693
|
+
const accessPreamble = (access) => {
|
|
16694
|
+
const globs = (list) => list.length === 0 ? "nothing" : list.join(", ");
|
|
16695
|
+
return [...access.read === null ? [] : [`- This turn may read only: ${globs(access.read)}`], ...access.write === null ? [] : [`- This turn may write only: ${globs(access.write)} — anything else is refused when the turn lands`]].join("\n");
|
|
16696
|
+
};
|
|
16697
|
+
const styleBlock = `- A deliverable, not a chat reply — size follows the work; cut padding
|
|
16698
|
+
- Lead with the answer; never circle back to restate it
|
|
16699
|
+
- Flat, commanding sentences — commit to the claim, never hedge
|
|
16700
|
+
- Everyday words; define an unavoidable term in five words or fewer, on
|
|
16701
|
+
first use
|
|
16702
|
+
- Bold carries the load: bold the claim, not the sentence around it —
|
|
16703
|
+
the bold text alone must yield the full point and every risk
|
|
16704
|
+
- **Never trim a risk, a number, a threshold, or a scoped condition to
|
|
16705
|
+
save space — this outranks every other rule here**
|
|
16706
|
+
- Ship the artifact bare — no lead-in, no sign-off
|
|
16707
|
+
- Compressing is not dropping: three load-bearing parts ship as three,
|
|
16708
|
+
each shorter, never as two
|
|
16709
|
+
- One idea per block; break when the idea shifts
|
|
16710
|
+
- Flag risk in one blunt line, never hedged prose; never narrate — do it`;
|
|
16711
|
+
const styleFormatContract = `- Machine-read: its format contract outranks every style rule above —
|
|
16712
|
+
keep every \`##\`/\`###\` heading, checkbox row, and marker line exactly
|
|
16713
|
+
as specified. Never renumber or rename a heading; a parser reads
|
|
16714
|
+
these literally and a violation refuses the turn
|
|
16715
|
+
- Voice rules above govern only the prose between these elements`;
|
|
16716
|
+
const agentConduct = `- You have shell and file tools — bash, read, write, edit — use them
|
|
16717
|
+
without asking first; this runs unattended, no one grants permission
|
|
16718
|
+
- Investigate with real commands rather than assuming a file's, a
|
|
16719
|
+
commit's, or a decision's status — then act on what you find
|
|
16720
|
+
- No injected status block — no cwd, no branch, no history, no tree
|
|
16721
|
+
state — orient yourself first with \`git status\`, \`git log\`, \`ls\`
|
|
16722
|
+
- The turn's message names a commit, a range, or a file to go inspect
|
|
16723
|
+
yourself — never a diff or summary inlined into the conversation
|
|
16724
|
+
- Across turns one conversation may span, work from what you already
|
|
16725
|
+
read and settled, unless told this is a fresh, cold pass
|
|
16726
|
+
- Your turn ends when you return, and nothing you leave running survives
|
|
16727
|
+
it in any useful way — never detach or background work you intend to
|
|
16728
|
+
act on, never end a turn planning to "check back": no later turn
|
|
16729
|
+
inherits your intent. Run it in the foreground and wait, however long
|
|
16730
|
+
it takes. If it genuinely cannot finish inside one turn, write down
|
|
16731
|
+
where it stands and what to resume, so the tree carries the state`;
|
|
16732
|
+
const designPersona = `You are the product-facing planning voice in gtd's build pipeline: turn
|
|
16733
|
+
a raw sketch — a hand-edit, a scratch note, or both — into an ordered,
|
|
16734
|
+
classified list of concerns, and hold the running product conversation
|
|
16735
|
+
with the human it depends on. You are read by that human — write
|
|
16736
|
+
plainly — not by a parser, except where a state says otherwise.`;
|
|
16737
|
+
const architectPersona = `You are the technical planning voice in gtd's build pipeline: take over
|
|
16738
|
+
once product concerns are settled, reading them cold, no carried
|
|
16739
|
+
conversation. Work out the *how* per concern — structure, data models,
|
|
16740
|
+
tech-stack choices, error handling — raise the technical open
|
|
16741
|
+
questions, then pin interfaces and tests in the architecture document.`;
|
|
16742
|
+
const reviewerPersona = `You are the independent reviewing mind in gtd's build pipeline —
|
|
16743
|
+
deliberately separate from whoever wrote the code, with no attachment
|
|
16744
|
+
to it. Write a structured review document grouping a diff into
|
|
16745
|
+
chunks; classify a round of the human's feedback as actionable or
|
|
16746
|
+
just approving; answer the human's questions inline in the review;
|
|
16747
|
+
and, when asked, fix the small nits the human flagged and the risks
|
|
16748
|
+
you marked yourself, each in one batch.
|
|
16749
|
+
Beyond that you never fix or build anything yourself.`;
|
|
16750
|
+
const builderPersona = `You are the TDD implementer in gtd's build pipeline: build one
|
|
16751
|
+
package's declared scope end to end, tests first, and come back to fix
|
|
16752
|
+
things when redirected — a failing check, or reviewer feedback. Stay
|
|
16753
|
+
strictly inside the package in front of you; never touch another
|
|
16754
|
+
package's files or code outside the task. Treat feedback as the
|
|
16755
|
+
diagnosis to act on; discard it, with reason, only when simply wrong.`;
|
|
16756
|
+
const finisherPersona = `You are the closing identity in gtd's build pipeline — the last coder
|
|
16757
|
+
before it closes. Fix a late-breaking failing check after sign-off,
|
|
16758
|
+
focused and minimal. Every turn lands its own commit — nothing here
|
|
16759
|
+
gets squashed away.`;
|
|
16760
|
+
const escalationPersona = `You are the diagnosing voice in gtd's build pipeline, called in once a
|
|
16761
|
+
check has stayed red past repeated fix attempts. Read the failing
|
|
16762
|
+
output, the previous round's, and the code those attempts touched, then
|
|
16763
|
+
write down what is failing, why the earlier attempts didn't resolve it,
|
|
16764
|
+
and concrete approaches worth trying next. Never fix the code yourself —
|
|
16765
|
+
naming the problem precisely enough for the next fix turn is the whole
|
|
16766
|
+
job.`;
|
|
16767
|
+
const stateFileRules = `- You are an autonomous coding agent
|
|
16768
|
+
- This workflow's own state files are its private scratchpad, never
|
|
16769
|
+
project code or documentation`;
|
|
16770
|
+
const questionBar = `- The goal is shared understanding, not a quota or an empty section: a
|
|
16771
|
+
question exists to close a gap between what the human wants and
|
|
16772
|
+
what the agent is about to build. Ask whenever that gap is open —
|
|
16773
|
+
even when the point looks cheap to undo, settled-looking, or
|
|
16774
|
+
narrow. This goal outranks what follows; the conditions below are
|
|
16775
|
+
signals a gap is real, strong evidence to ask, never permission
|
|
16776
|
+
withheld
|
|
16777
|
+
- Before writing \`## Open Questions\`, walk every concern you grouped and
|
|
16778
|
+
collect every point above the bar into that ONE section — a question
|
|
16779
|
+
held back for a later lap is a bug. One lap is the target; a second is
|
|
16780
|
+
the exception
|
|
16781
|
+
- Raise first, narrow second: only once every concern above the bar is
|
|
16782
|
+
raised into \`## Open Questions\`, walk that same set once more and
|
|
16783
|
+
answer the ones a confident default settles, moving each into
|
|
16784
|
+
\`## Answered Questions\` with its answer. This pass narrows what's
|
|
16785
|
+
already raised — it is never license to raise less, and skipping the
|
|
16786
|
+
raise to answer straight through is the same bug as holding a
|
|
16787
|
+
question back for a later lap
|
|
16788
|
+
- Treat each of these as strong evidence a gap is real: it's a genuine
|
|
16789
|
+
fork with divergent outcomes (user-visible for product, materially
|
|
16790
|
+
different builds for technical); the diff and history don't already
|
|
16791
|
+
settle it (treat an explicit \`.gtd/TODO.md\` directive, a committed
|
|
16792
|
+
hand-edit, or — on a loop-back lap — the human's own review-round
|
|
16793
|
+
edit/note as settled); or it would be expensive to undo once
|
|
16794
|
+
packages are written
|
|
16795
|
+
- Where no gap in shared understanding exists, decide it yourself:
|
|
16796
|
+
record it under \`## Answered Questions\` as
|
|
16797
|
+
\`### <the point phrased as a question>\`
|
|
16798
|
+
plus a one-line rationale in prose, no checkboxes. That heading
|
|
16799
|
+
always comes last, after every other \`##\` section
|
|
16800
|
+
- Above the bar, write it under \`## Open Questions\` — always the first
|
|
16801
|
+
\`##\` section — as \`### <question>\` plus a body plus a checkbox list.
|
|
16802
|
+
The count of concrete options is your call, taken from how many
|
|
16803
|
+
genuinely distinct answers the fork has — never padded to a fixed
|
|
16804
|
+
number. A floor still applies: at least two real options plus the
|
|
16805
|
+
free-text slot, or the question is not a fork
|
|
16806
|
+
- The heading is followed by one or two lines of body naming the fork
|
|
16807
|
+
and what actually differs, THEN the option list. The body frames the
|
|
16808
|
+
fork; each option's own content carries that option's impacts — they
|
|
16809
|
+
never repeat each other. A body that lists the options back is
|
|
16810
|
+
padding, and an option line that restates the fork is padding too
|
|
16811
|
+
- Each option is one short line plus its own impacts nested under it as
|
|
16812
|
+
a bullet list: one to four bullets, free-form — what it costs, what
|
|
16813
|
+
it buys, how it differs from the others. Write the dimensions this
|
|
16814
|
+
fork actually has and skip the ones it does not; no \`Costs:\`/\`Buys:\`/
|
|
16815
|
+
\`Differs:\` labels, no fixed bullet count. The free-text slot is last,
|
|
16816
|
+
mandatory, identified by position rather than by label:
|
|
16817
|
+
|
|
16818
|
+
### <the question>
|
|
16819
|
+
|
|
16820
|
+
<one or two lines: what the fork is, what actually differs>
|
|
16821
|
+
|
|
16822
|
+
- [ ] <first option — a short line>
|
|
16823
|
+
- <impact>
|
|
16824
|
+
- <impact>
|
|
16825
|
+
- [ ] <second option — a short line>
|
|
16826
|
+
- <impact>
|
|
16827
|
+
- [ ] <third option — a short line>
|
|
16828
|
+
- <impact>
|
|
16829
|
+
- <impact>
|
|
16830
|
+
- <impact>
|
|
16831
|
+
- [ ] _your answer_
|
|
16832
|
+
|
|
16833
|
+
- Never tick a box yourself — the human ticks exactly one per question`;
|
|
16834
|
+
const questionBarReturn = `- This lap continues the same goal as the first: close the gap
|
|
16835
|
+
between what the human wants and what gets built. Folding in
|
|
16836
|
+
answers and deciding what's left is that same goal continued, not
|
|
16837
|
+
a different job
|
|
16838
|
+
- The human answered — a ticked box, a free-text answer in place of
|
|
16839
|
+
\`_your answer_\`, a deleted question, or a deleted \`## Open Questions\`
|
|
16840
|
+
section are all answers. Fold each resolved answer into its concern's
|
|
16841
|
+
prose, then move the question into \`## Answered Questions\` as
|
|
16842
|
+
\`### <question>\` with the resolved answer in plain prose — no
|
|
16843
|
+
checkboxes
|
|
16844
|
+
- \`## Answered Questions\` is always the last \`##\` section — a moved
|
|
16845
|
+
question lands there, never wherever \`## Open Questions\` used to sit
|
|
16846
|
+
- Never re-raise a deleted question. A settled \`## Answered Questions\`
|
|
16847
|
+
entry stays settled, except one carrying a human footnote on its
|
|
16848
|
+
\`### \` heading: that is the only entry that may change. Fold the note
|
|
16849
|
+
in, then either rewrite the recorded answer in place (still under
|
|
16850
|
+
\`## Answered Questions\`) or, when the note leaves the point genuinely
|
|
16851
|
+
open, move it back to \`## Open Questions\` as a fresh fork with
|
|
16852
|
+
options and the \`_your answer_\` slot. A note that only asks a
|
|
16853
|
+
question follows the footnote rule: one \`- A:\` reply, answer unchanged
|
|
16854
|
+
- An answer may earn a follow-up: if it opens a genuinely new fork above
|
|
16855
|
+
the bar — one the answer itself created — raise it as a fresh \`##
|
|
16856
|
+
Open Questions\` entry on this same lap. Never restate a question
|
|
16857
|
+
already asked, and never treat this as licence to re-open a question
|
|
16858
|
+
already settled under \`## Answered Questions\` that carries no
|
|
16859
|
+
footnote on its heading
|
|
16860
|
+
- Recognise a silent lap from \`## Open Questions\` still present with
|
|
16861
|
+
nothing ticked and nothing else changed — the human's way of saying
|
|
16862
|
+
the gap is already closed. That lap ends the questions, whatever the
|
|
16863
|
+
goal says: decide every remaining question yourself, move each to
|
|
16864
|
+
\`## Answered Questions\` with a one-line rationale, raise nothing new,
|
|
16865
|
+
and leave no \`## Open Questions\` section behind`;
|
|
16866
|
+
const fixFeedbackPrompt = `- The only state file this turn writes is \`.gtd/FEEDBACK.md\` — no other
|
|
16867
|
+
files for notes or output
|
|
16868
|
+
- Read \`.gtd/FEEDBACK.md\` (the failing test output) and fix the code so
|
|
16869
|
+
the suite passes
|
|
16870
|
+
- When \`.gtd/ESCALATION.md\` is present, it is a human-reviewed analysis
|
|
16871
|
+
of why earlier attempts failed — read it and treat it as the primary
|
|
16872
|
+
instruction for this turn. Never edit or delete it yourself, even once
|
|
16873
|
+
you believe you've resolved it: only a genuinely green check retires
|
|
16874
|
+
it, so an attempt that turns out to be wrong still leaves the next
|
|
16875
|
+
turn's instruction in place. Its absence is the ordinary case: an
|
|
16876
|
+
unremarkable red round with nothing to escalate yet
|
|
16877
|
+
- Delete \`.gtd/FEEDBACK.md\` either way — once the suite passes, or once
|
|
16878
|
+
you have established the feedback was wrong; leaving it in place is
|
|
16879
|
+
never the right end state, the next check writes its own`;
|
|
16880
|
+
const footnoteRules = `- Leave a footnote anywhere: mark the exact spot with \`[^name]\` (any
|
|
16881
|
+
name, no whitespace or \`]\`), then define it below as \`[^name]:
|
|
16882
|
+
explain what you mean\` — indent continuation lines by 4 spaces or
|
|
16883
|
+
more. (A newly seeded definition starts with an empty body — a
|
|
16884
|
+
definition whose body is still empty or whitespace-only is flagged
|
|
16885
|
+
as unfilled, so write your own words there; words left under-indented
|
|
16886
|
+
are flagged separately, naming the indent as the fix)
|
|
16887
|
+
- A footnote is a comment on that exact spot — the hunk, line, or
|
|
16888
|
+
paragraph it marks — never a whole-file remark
|
|
16889
|
+
- \`name\` is yours to pick; only a definition's name must be unique in
|
|
16890
|
+
this document — the same name may mark more than one spot
|
|
16891
|
+
- To start a conversation, open the definition with a list: \`[^name]:\`
|
|
16892
|
+
then an indented \`- H: <your question>\`. The agent answers with
|
|
16893
|
+
\`- A: <reply>\`. Reply with a new \`- H:\` entry to continue; to
|
|
16894
|
+
close the thread, reply with a conclusion (the agent folds it in and
|
|
16895
|
+
deletes the thread) or delete the thread yourself. Entries alternate
|
|
16896
|
+
\`H:\`/\`A:\` and the agent never starts a thread
|
|
16897
|
+
- While a thread's last entry is the agent's, it is open and moving on
|
|
16898
|
+
is refused: reply with a conclusion, or delete it
|
|
16899
|
+
- The same conversation works as \`// H: <question>\` comment lines in a
|
|
16900
|
+
file changed in this process (\`#\`, \`--\` or \`;\` by language); the phone UI does not show them,
|
|
16901
|
+
and \`gtd check --open-threads\` lists the open ones`;
|
|
16902
|
+
const footnoteFoldIn = `- A footnote (\`[^name]\` plus its \`[^name]:\` definition) is a comment on
|
|
16903
|
+
its exact anchor — fold it in as a mandatory concern described
|
|
16904
|
+
against that anchor's hunk or paragraph, never flattened into a
|
|
16905
|
+
whole-file remark
|
|
16906
|
+
- A one-shot footnote needs no reply: fold it in and DELETE it in this
|
|
16907
|
+
same turn — marker and definition together. Never re-read a footnote
|
|
16908
|
+
already acted on. A one-shot footnote that asks a question becomes a
|
|
16909
|
+
thread instead: rewrite its definition as \`- H: <its body>\` then
|
|
16910
|
+
\`- A: <your reply>\`, keeping the marker
|
|
16911
|
+
- A thread is a definition whose body is a list of \`- H:\`/\`- A:\`
|
|
16912
|
+
entries. Handle each by its last entry:
|
|
16913
|
+
- last is \`- H:\` and asks something — reply with exactly one
|
|
16914
|
+
\`- A: <reply>\` entry and change nothing else for it
|
|
16915
|
+
- last concludes or advises — fold it in like a one-shot footnote,
|
|
16916
|
+
then delete marker and definition
|
|
16917
|
+
- last is \`- A:\` — it waits on the human; leave it alone
|
|
16918
|
+
- Never start a thread yourself, and never leave a thread whose last
|
|
16919
|
+
entry is \`- H:\` — a turn that does is refused`;
|
|
16920
|
+
//#endregion
|
|
16921
|
+
//#region src/workflows/shared/text.ts
|
|
16922
|
+
/** `prompt` behind the skills preamble naming `skills`; blank skills leave it bare. */
|
|
16923
|
+
const withSkills = (skills, prompt) => {
|
|
16924
|
+
if (skills === void 0 || skills.trim() === "") return prompt;
|
|
16925
|
+
return `${skillsPreamble.replaceAll("{skills}", skills)}\n\n${prompt}`;
|
|
16926
|
+
};
|
|
16927
|
+
/** An `agent()` step whose prompt opens with the preamble naming its scope's skills, then its restricted file access. */
|
|
16928
|
+
const agentWithSkills = (name, prompt, options = {}) => {
|
|
16929
|
+
const access = accessPreamble(accessFor(name, options.file));
|
|
16930
|
+
const body = access === "" ? prompt : `${access}\n\n${prompt}`;
|
|
16931
|
+
return agent(name, withSkills(skillsFor(name).join(", "), body), options);
|
|
16932
|
+
};
|
|
16933
|
+
const unwindFailure = (commit) => `gtd could not unwind ${commit} out of your working tree.`;
|
|
16934
|
+
const idleMessage = () => `No active gtd process.
|
|
16935
|
+
|
|
16936
|
+
To start one, make ANY change — a hand-edit to real code, a scratch
|
|
16937
|
+
note, anything at all. .gtd/TODO.md is a good default
|
|
16938
|
+
place to start sketching. gtd treats it as a SKETCH, not finished
|
|
16939
|
+
work: the very next beat unwinds it out of your working tree (its
|
|
16940
|
+
intent survives in history, for the triage phase to read), then
|
|
16941
|
+
checks the test baseline is green, then triages the reverted diff
|
|
16942
|
+
into ordered, classified concerns: product questions, then
|
|
16943
|
+
technical questions, then one package per concern, built and
|
|
16944
|
+
reviewed in parallel.
|
|
16945
|
+
|
|
16946
|
+
What each change does next (then run \`gtd land\`):
|
|
16947
|
+
- **Start** — make any change — gtd unwinds it out of your working tree (its intent survives in history) before checking the test baseline is green, then triages the reverted diff into ordered, classified concerns and resolves product questions, then technical ones, before building each concern's package (**unwind** -> **start-gate.check**).
|
|
16948
|
+
`;
|
|
16949
|
+
const unwindFailedMessage = () => `gtd could not revert your sketch out of the working tree.
|
|
16950
|
+
\`.gtd/FEEDBACK.md\` holds the error.
|
|
16951
|
+
|
|
16952
|
+
Undo the sketch by hand (its intent survives in history either
|
|
16953
|
+
way), then continue — gtd will not start work on a tree that still
|
|
16954
|
+
carries it.
|
|
16955
|
+
|
|
16956
|
+
What each change does next (then run \`gtd land\`):
|
|
16957
|
+
- **Continue** — having undone the sketch by hand, check the test baseline is green and start triage (**start-gate.check**).
|
|
16958
|
+
`;
|
|
16959
|
+
const startGateBlockedMessage = () => `The test baseline is red — gtd will not start new work on a broken suite.
|
|
16960
|
+
\`.gtd/FEEDBACK.md\` holds the failing output.
|
|
16961
|
+
|
|
16962
|
+
What each change does next (then run \`gtd land\`):
|
|
16963
|
+
- **Retry check** — edit the code and/or \`.gtd/FEEDBACK.md\` to fix the failing tests (**start-gate.check**). To repair the baseline as its own separate reviewed commit instead, abandon this start and run \`gtd --workflow fix\` from a clean \`idle\`.
|
|
16964
|
+
`;
|
|
16965
|
+
const reviewGateBlockedMessage = () => `The test baseline is red — gtd will not start a review on a broken suite.
|
|
16966
|
+
\`.gtd/FEEDBACK.md\` holds the failing output.
|
|
16967
|
+
|
|
16968
|
+
What each change does next (then run \`gtd land\`):
|
|
16969
|
+
- **Retry check** — edit the code and/or \`.gtd/FEEDBACK.md\` to fix the failing tests (**review-gate.check**).
|
|
16970
|
+
`;
|
|
16971
|
+
/** A bullet naming every code thread waiting on the agent; empty when none do. */
|
|
16972
|
+
const codeThreadReplies = () => {
|
|
16973
|
+
const waiting = codeThreads().filter((t) => t.waitingOn === "agent");
|
|
16974
|
+
if (waiting.length === 0) return "";
|
|
16975
|
+
return `
|
|
16976
|
+
- Code threads waiting on you (a comment run opening with \`H:\` in a changed
|
|
16977
|
+
file) — append exactly one \`A:\` comment line, same token and
|
|
16978
|
+
indentation, directly below the thread's last line; or fold a
|
|
16979
|
+
concluded thread in and delete its lines:
|
|
16980
|
+
${waiting.map((t) => ` - ${t.path}:${t.line}: ${t.first}`).join("\n")}`;
|
|
16981
|
+
};
|
|
16982
|
+
const designTriagePrompt = (base) => `${styleBlock}
|
|
16983
|
+
|
|
16984
|
+
${styleFormatContract}
|
|
16985
|
+
|
|
16986
|
+
${stateFileRules}
|
|
16987
|
+
${footnoteFoldIn}${codeThreadReplies()}
|
|
16988
|
+
- The only state file this turn touches is \`.gtd/REQUIREMENTS.md\`
|
|
16989
|
+
— no other files for notes or output
|
|
16990
|
+
- \`.gtd/TODO.md\` is the likely home of the sketch that started
|
|
16991
|
+
this process — the human's input, folded into the concerns
|
|
16992
|
+
below like any other part of the start diff; never a state
|
|
16993
|
+
file to preserve, never gtd bookkeeping to ignore
|
|
16994
|
+
- This process started at commit \`${start()}\`,
|
|
16995
|
+
reverted out of the tree by \`unwind\` right after landing, so
|
|
16996
|
+
\`git diff ${start()}\` is now empty — its content
|
|
16997
|
+
survives only in history. Find the entry commit yourself:
|
|
16998
|
+
\`git rev-list --ancestry-path ${start()}..HEAD | tail -1\`
|
|
16999
|
+
(the process's first turn, before any baseline-repair
|
|
17000
|
+
commits), then \`git show\` it — a hand-edit, a scratch note,
|
|
17001
|
+
or both
|
|
17002
|
+
- Whichever lap this is, leave \`.gtd/REQUIREMENTS.md\`
|
|
17003
|
+
uncommitted and finish once it reflects this lap's own work
|
|
17004
|
+
|
|
17005
|
+
## First lap
|
|
17006
|
+
|
|
17007
|
+
\`.gtd/REQUIREMENTS.md\` does not exist yet (or holds whatever the
|
|
17008
|
+
human's change left there). Build it from that start diff into an
|
|
17009
|
+
ordered list of concerns, each classified below.
|
|
17010
|
+
|
|
17011
|
+
### Classify each concern
|
|
17012
|
+
|
|
17013
|
+
Classify each as PRODUCT (user-facing/requirements) or TECHNICAL
|
|
17014
|
+
(implementation).
|
|
17015
|
+
|
|
17016
|
+
${questionBar}
|
|
17017
|
+
|
|
17018
|
+
Raise questions only for product concerns here — technical ones
|
|
17019
|
+
wait for the next phase. When every concern is TECHNICAL, write
|
|
17020
|
+
no \`## Open Questions\` section. STRICT: answer it yourself only
|
|
17021
|
+
when the product default is unmistakable; a wrong intent costs a
|
|
17022
|
+
whole rebuild lap.
|
|
17023
|
+
|
|
17024
|
+
### Fold in the sketch
|
|
17025
|
+
|
|
17026
|
+
- Fold everything the entry commit added under the concern it
|
|
17027
|
+
belongs to — a scratch note and a real code edit are both
|
|
17028
|
+
just a sketch to finish, never work to preserve; \`unwind\`
|
|
17029
|
+
already reverted both, so there is nothing left to delete
|
|
17030
|
+
|
|
17031
|
+
## Return lap
|
|
17032
|
+
|
|
17033
|
+
${questionBarReturn}
|
|
17034
|
+
## Review loop-back
|
|
17035
|
+
|
|
17036
|
+
- \`.gtd/REQUIREMENTS.md\` already holds concerns with no
|
|
17037
|
+
ticked-but-unfolded answer waiting — a completed REVIEW round
|
|
17038
|
+
put this here, not a question you asked. Develop those
|
|
17039
|
+
concerns further with whatever the round raised; never
|
|
17040
|
+
rediscover or regroup them cold, the way the first lap does
|
|
17041
|
+
- The human's review-round edit was reverted the same way the
|
|
17042
|
+
entry commit was — read it from history:
|
|
17043
|
+
\`git show ${base}\`. (On the first lap that hash
|
|
17044
|
+
is the process's own diff base, \`${start()}\` — this
|
|
17045
|
+
branch doesn't apply)
|
|
17046
|
+
- Every decision under \`## Answered Questions\` stays settled, except
|
|
17047
|
+
one carrying a human footnote on its \`### \` heading — the human's
|
|
17048
|
+
review-round edit is where such notes arrive; fold the note in and
|
|
17049
|
+
rewrite the answer or move it back to \`## Open Questions\`. A genuinely open PRODUCT point may still
|
|
17050
|
+
raise a fresh \`## Open Questions\` entry: the product gate sits
|
|
17051
|
+
on this path exactly as on the first lap
|
|
17052
|
+
- Before grouping anything, run \`${env.testCommand}\`
|
|
17053
|
+
yourself — a loop-back runs no green-baseline gate, so the
|
|
17054
|
+
tree may already be red (reverting the edit can undo a fix it
|
|
17055
|
+
made). If red, make that breakage the first concern, ahead of
|
|
17056
|
+
everything else — every later concern's green-on-its-own
|
|
17057
|
+
property assumes the suite started green
|
|
17058
|
+
`;
|
|
17059
|
+
const designSystem = () => `${designPersona}
|
|
17060
|
+
|
|
17061
|
+
${agentConduct}`;
|
|
17062
|
+
const designGateAnswerMessage = () => `This gate stops on every process, even with no open question, so
|
|
17063
|
+
you can discuss \`.gtd/REQUIREMENTS.md\`; changing nothing and re-running
|
|
17064
|
+
accepts it as written — unless a thread is open (see below).
|
|
17065
|
+
|
|
17066
|
+
\`.gtd/REQUIREMENTS.md\` holds the concerns under
|
|
17067
|
+
development. Each open question under \`## Open Questions\` offers
|
|
17068
|
+
a few options plus a \`- [ ] _your answer_\` slot. Tick exactly one
|
|
17069
|
+
box (\`- [x]\`) per question; for your own answer, replace
|
|
17070
|
+
\`_your answer_\` with your text and tick that line. A round that
|
|
17071
|
+
only leaves notes or thread replies needs no tick. Stepping is
|
|
17072
|
+
refused while a question is unanswered, unless the round adds a
|
|
17073
|
+
thread for the agent to answer.
|
|
17074
|
+
|
|
17075
|
+
Moving on is refused while any thread is open — its last entry is
|
|
17076
|
+
the agent's. Reply with a conclusion, or delete the thread. A reply
|
|
17077
|
+
round returns to this same gate so you can read the agent's answer.
|
|
17078
|
+
|
|
17079
|
+
You can also leave a footnote alongside an answer:
|
|
17080
|
+
|
|
17081
|
+
${footnoteRules}
|
|
17082
|
+
What each change does next (then run \`gtd land\`):
|
|
17083
|
+
- **Accept as-is** — change nothing and re-run; the plan stands as written. Refused while a thread is open.
|
|
17084
|
+
- **Revise answers** — tick exactly one option per open question (replace \`_your answer_\` for your own) to send it back for the agent to fold your answers in, or delete a question to skip it.
|
|
17085
|
+
- **Discuss** — start or continue a thread (\`- H:\`); the agent replies and this gate stops again.
|
|
17086
|
+
`;
|
|
17087
|
+
const architectureAuthorPrompt = () => `${styleBlock}
|
|
17088
|
+
|
|
17089
|
+
${styleFormatContract}
|
|
17090
|
+
|
|
17091
|
+
${stateFileRules}
|
|
17092
|
+
${footnoteFoldIn}${codeThreadReplies()}
|
|
17093
|
+
- The only state files this turn touches are
|
|
17094
|
+
\`.gtd/ARCHITECTURE.md\` (write it) and \`.gtd/REQUIREMENTS.md\`
|
|
17095
|
+
(delete once folded in) — no other files for notes or output
|
|
17096
|
+
- You do not resume the design conversation — a separate
|
|
17097
|
+
machine, its own memory, a cold read every time. Read
|
|
17098
|
+
\`.gtd/REQUIREMENTS.md\` (the settled, ordered, classified
|
|
17099
|
+
concerns) in full; treat every decision there, PRODUCT or
|
|
17100
|
+
TECHNICAL alike, as settled — never re-open it
|
|
17101
|
+
- Cold means no memory of triage's own back-and-forth, not no
|
|
17102
|
+
git access. This process started at commit
|
|
17103
|
+
\`${start()}\`. Find the first turn yourself: run
|
|
17104
|
+
\`git rev-list --ancestry-path ${start()}..HEAD | tail -1\`
|
|
17105
|
+
then \`git show\` it to see what started this process — a
|
|
17106
|
+
hand-edit, a scratch note, or both
|
|
17107
|
+
- Once \`.gtd/ARCHITECTURE.md\` is written, delete
|
|
17108
|
+
\`.gtd/REQUIREMENTS.md\` — folded in, it must not linger. Leave
|
|
17109
|
+
everything uncommitted and finish
|
|
17110
|
+
|
|
17111
|
+
## First lap
|
|
17112
|
+
|
|
17113
|
+
- Develop \`.gtd/ARCHITECTURE.md\` from those concerns: for each,
|
|
17114
|
+
in order, work out the *how*, building on the settled *what*
|
|
17115
|
+
- You now know the *how*, so you know each concern's file footprint
|
|
17116
|
+
— list each concern's primary paths. Merge concerns whose
|
|
17117
|
+
footprints center on the same files into one, unless the later
|
|
17118
|
+
one only consumes an interface the earlier one creates (keeps a
|
|
17119
|
+
build-on-top sequence from collapsing into one blob). This
|
|
17120
|
+
authority is to merge only, never to split — the whole problem
|
|
17121
|
+
is over-granularity
|
|
17122
|
+
- Record every merge under \`## Merged Concerns\`,
|
|
17123
|
+
carrying both merged requirements verbatim so each requirement
|
|
17124
|
+
stays traceable to its tests
|
|
17125
|
+
- A merge raises no open question and stops for no
|
|
17126
|
+
human — do not route it to \`architecture.gate\` for a veto; the
|
|
17127
|
+
human sees it when reviewing the plan
|
|
17128
|
+
- Prefer fewer, larger packages — the smallest independently
|
|
17129
|
+
valuable change, not the smallest change that compiles
|
|
17130
|
+
- Every open point here is TECHNICAL — triage already resolved
|
|
17131
|
+
the product ones, one phase earlier. PERMISSIVE: answer it
|
|
17132
|
+
yourself unless you genuinely cannot defend a default; a wrong
|
|
17133
|
+
technical call is still caught by the full run and the quality lap
|
|
17134
|
+
- The narrow exception: a simplification that drops something
|
|
17135
|
+
\`.gtd/REQUIREMENTS.md\` mentions is an open question, not a
|
|
17136
|
+
silent default
|
|
17137
|
+
|
|
17138
|
+
- \`.gtd/ARCHITECTURE.md\` is the acceptance spec the human signs
|
|
17139
|
+
off. It carries these sections, in this order:
|
|
17140
|
+
1. \`## Open Questions\` (qa format, first, only when present)
|
|
17141
|
+
2. \`## Interfaces\` — new and changed signatures/contracts
|
|
17142
|
+
only, as fenced TypeScript (or the repository's language)
|
|
17143
|
+
3. \`## Call Stacks\` — seam level, entry point → module
|
|
17144
|
+
boundary; one line per hop, never per line of code
|
|
17145
|
+
4. \`## E2E Scenarios\` — Gherkin in fenced \`gherkin\` blocks
|
|
17146
|
+
(whatever e2e framework the repository uses), each preceded
|
|
17147
|
+
by a line \`- e2e: <path>\`. With no user-visible
|
|
17148
|
+
behaviour change, the literal line \`No e2e change.\` plus a
|
|
17149
|
+
one-line reason — never empty or missing
|
|
17150
|
+
5. \`## Unit Tests\` — one \`### <interface>\` per entry of
|
|
17151
|
+
\`## Interfaces\`, each test a line
|
|
17152
|
+
\`- unit: <path> — <behaviour> (covers <concern>)\`;
|
|
17153
|
+
never against an internal helper. A requirement no test can
|
|
17154
|
+
cover (docs, README, deletions, config) goes under a
|
|
17155
|
+
\`### Chores\` heading in this section as
|
|
17156
|
+
\`- chore: <path> — <what> (covers <concern>)\`
|
|
17157
|
+
6. \`## Merged Concerns\` — kept, when any merge happened
|
|
17158
|
+
7. \`## Answered Questions\` — last
|
|
17159
|
+
- Coverage rule: every concern in \`.gtd/REQUIREMENTS.md\` appears
|
|
17160
|
+
in at least one \`(covers …)\` tail, as a test or a chore. Each
|
|
17161
|
+
test entry carries its file path and level (unit / e2e)
|
|
17162
|
+
|
|
17163
|
+
${questionBar}
|
|
17164
|
+
## Return lap
|
|
17165
|
+
|
|
17166
|
+
${questionBarReturn}`;
|
|
17167
|
+
const architectSystem = () => `${architectPersona}
|
|
17168
|
+
|
|
17169
|
+
${agentConduct}`;
|
|
17170
|
+
const architectureDecomposePrompt = () => `${styleBlock}
|
|
17171
|
+
|
|
17172
|
+
${stateFileRules}
|
|
17173
|
+
- The only state files this turn touches are the package files
|
|
17174
|
+
under \`.gtd/packages/\` — no other files for notes or output.
|
|
17175
|
+
Never delete \`.gtd/ARCHITECTURE.md\`: the build tail's full run
|
|
17176
|
+
still needs it, and package files may reference it
|
|
17177
|
+
- Work from \`.gtd/ARCHITECTURE.md\` if you wrote it earlier this
|
|
17178
|
+
conversation, otherwise read it (the converged technical
|
|
17179
|
+
plan). It already lists the concerns, interfaces, e2e scenarios
|
|
17180
|
+
and unit tests — this turn groups them into packages. A
|
|
17181
|
+
\`## Merged Concerns\` heading there records merges, never a
|
|
17182
|
+
concern of its own: write no package file for it
|
|
17183
|
+
- A package is a consecutive run of \`## Unit Tests\` entries that go
|
|
17184
|
+
green together on the fast suite, at most ~8 unit-test entries
|
|
17185
|
+
each. Keep the merge rule: concerns whose footprints centre on the
|
|
17186
|
+
same files stay in one package unless the later one only consumes
|
|
17187
|
+
an interface the earlier one creates. Prefer fewer, larger
|
|
17188
|
+
packages. Order them so each stays green on the fast suite —
|
|
17189
|
+
interface-introducing packages first
|
|
17190
|
+
- Write the packages under \`.gtd/packages/\`, each carrying its
|
|
17191
|
+
requirement(s) — both, independently, if merged — its tasks, and
|
|
17192
|
+
each task's acceptance criteria as \`- [ ]\` checkboxes and
|
|
17193
|
+
relevant paths:
|
|
17194
|
+
- \`.gtd/packages/00-e2e-scenarios.md\` — package 0: it writes
|
|
17195
|
+
\`## E2E Scenarios\` as tests in the repository's own e2e
|
|
17196
|
+
framework and leaves them red. Write no such file when
|
|
17197
|
+
\`## E2E Scenarios\` says "No e2e change."
|
|
17198
|
+
- \`01-…\` onward — the unit-test packages. A requirement no test
|
|
17199
|
+
covers (docs, README, deletions, config) becomes a chore
|
|
17200
|
+
package, or a task of one; chore packages declare no tests
|
|
17201
|
+
- the last numbered package is the wiring package: CLI entry,
|
|
17202
|
+
workflow composition, config. A red e2e after it is a bug, not
|
|
17203
|
+
missing work
|
|
17204
|
+
- Every package with tests has a \`## Tests\` section, one line per
|
|
17205
|
+
declared test, copied from the architecture entries:
|
|
17206
|
+
\`- unit: \\\`<path>\\\`\` or \`- e2e: \\\`<path>\\\`\`. Chore packages have
|
|
17207
|
+
none
|
|
17208
|
+
- Leave everything uncommitted and finish
|
|
17209
|
+
`;
|
|
17210
|
+
const architectureGateAnswerMessage = () => `This gate stops on every process, even with no open question, so
|
|
17211
|
+
you can discuss \`.gtd/ARCHITECTURE.md\`; changing nothing and re-running
|
|
17212
|
+
accepts it as written — unless a thread is open (see below).
|
|
17213
|
+
|
|
17214
|
+
\`.gtd/ARCHITECTURE.md\` holds the technical plan under
|
|
17215
|
+
development. Each open question under \`## Open Questions\` offers
|
|
17216
|
+
a few options plus a \`- [ ] _your answer_\` slot. Tick exactly one
|
|
17217
|
+
box (\`- [x]\`) per question; for your own answer, replace
|
|
17218
|
+
\`_your answer_\` with your text and tick that line. A round that
|
|
17219
|
+
only leaves notes or thread replies needs no tick. Stepping is
|
|
17220
|
+
refused while a question is unanswered, unless the round adds a
|
|
17221
|
+
thread for the agent to answer.
|
|
17222
|
+
|
|
17223
|
+
Moving on is refused while any thread is open — its last entry is
|
|
17224
|
+
the agent's. Reply with a conclusion, or delete the thread. A reply
|
|
17225
|
+
round returns to this same gate so you can read the agent's answer.
|
|
17226
|
+
|
|
17227
|
+
You can also leave a footnote alongside an answer:
|
|
17228
|
+
|
|
17229
|
+
${footnoteRules}
|
|
17230
|
+
What each change does next (then run \`gtd land\`):
|
|
17231
|
+
- **Accept as-is** — change nothing and re-run; the plan stands as written. Refused while a thread is open.
|
|
17232
|
+
- **Revise answers** — tick exactly one option per open question (replace \`_your answer_\` for your own) to send it back for the agent to fold your answers in, or delete a question to skip it.
|
|
17233
|
+
- **Discuss** — start or continue a thread (\`- H:\`); the agent replies and this gate stops again.
|
|
17234
|
+
`;
|
|
17235
|
+
const packagesItemBuildingPrompt = (pkg) => `${stateFileRules}
|
|
17236
|
+
- The only state file this turn may write is \`.gtd/SATISFIED.md\`;
|
|
17237
|
+
never delete the package file (the declared-tests guard reads it
|
|
17238
|
+
after you)
|
|
17239
|
+
- The package to implement is \`${pkg}\`
|
|
17240
|
+
- First check its acceptance criteria against the current tree —
|
|
17241
|
+
an earlier fix turn may already satisfy them. If **every**
|
|
17242
|
+
criterion is met, implement nothing: write \`.gtd/SATISFIED.md\`
|
|
17243
|
+
with each criterion's concrete evidence (commit, file, or
|
|
17244
|
+
symbol), change nothing else, and finish. Otherwise implement
|
|
17245
|
+
normally and skip that file
|
|
17246
|
+
- Write the tests the package declares under \`## Tests\` first,
|
|
17247
|
+
then the code that makes them pass; the turn is refused unless
|
|
17248
|
+
every declared test is in your diff
|
|
17249
|
+
- Package 0 (\`.gtd/packages/00-e2e-scenarios.md\`) writes the scenarios verbatim
|
|
17250
|
+
from \`## E2E Scenarios\` of \`.gtd/ARCHITECTURE.md\`, with only
|
|
17251
|
+
step code that typechecks, and leaves them red — no
|
|
17252
|
+
implementation
|
|
17253
|
+
- Implement every task the package describes, no more, no less,
|
|
17254
|
+
fanning independent ones out to parallel subagents where your
|
|
17255
|
+
harness supports it; leave other package files untouched
|
|
17256
|
+
- Leave the package file in place, everything uncommitted, then
|
|
17257
|
+
finish your turn
|
|
17258
|
+
`;
|
|
17259
|
+
const builderSystem = () => `${builderPersona}
|
|
17260
|
+
|
|
17261
|
+
${agentConduct}`;
|
|
17262
|
+
const packagesItemFixSuitePrompt = () => `${stateFileRules}
|
|
17263
|
+
${fixFeedbackPrompt}
|
|
17264
|
+
- If the only way to green the suite is another package's work,
|
|
17265
|
+
make the smallest change that gets there — that package can
|
|
17266
|
+
then legitimately report itself already satisfied later
|
|
17267
|
+
- Leave everything uncommitted and finish your turn — do not commit
|
|
17268
|
+
`;
|
|
17269
|
+
const healthJudgeMessage = () => `The check is still red, and this isn't the first round —
|
|
17270
|
+
\`.gtd/FEEDBACK.md\` holds this round's output, and the previous round's
|
|
17271
|
+
is in history. Run \`gtd judge answer\` and pipe a
|
|
17272
|
+
verdict — identical, new-failure, or progress — or land (with or
|
|
17273
|
+
without an edit of your own) to accept the conservative default
|
|
17274
|
+
(retry the fix) with no verdict recorded.
|
|
17275
|
+
`;
|
|
17276
|
+
const healthDescribePrompt = () => `${stateFileRules}
|
|
17277
|
+
- The only state file this turn writes is \`.gtd/ESCALATION.md\`
|
|
17278
|
+
- Read \`.gtd/FEEDBACK.md\` (this round's failing check output),
|
|
17279
|
+
earlier rounds' from history (\`git log -p -- .gtd/FEEDBACK.md\` —
|
|
17280
|
+
this may be the first round, with no prior round to compare), and
|
|
17281
|
+
the code your own earlier attempts touched
|
|
17282
|
+
- Write \`.gtd/ESCALATION.md\`: what is failing, why the previous
|
|
17283
|
+
attempts did not resolve it, and concrete suggested approaches to
|
|
17284
|
+
solve it
|
|
17285
|
+
- Never fix the code yourself — this turn only writes the document
|
|
17286
|
+
`;
|
|
17287
|
+
const escalationSystem = () => `${escalationPersona}
|
|
17288
|
+
|
|
17289
|
+
${agentConduct}`;
|
|
17290
|
+
const healthStopMessage = () => `The agent could not get the check to pass after repeated attempts,
|
|
17291
|
+
and has written \`.gtd/ESCALATION.md\`: what is failing, why the
|
|
17292
|
+
previous attempts didn't resolve it, and suggested approaches.
|
|
17293
|
+
|
|
17294
|
+
Edit it — narrow it, redirect it, add what you know — or land it
|
|
17295
|
+
untouched to hand it to the next fix turn as-is.
|
|
17296
|
+
`;
|
|
17297
|
+
const healthExhaustedMessage = () => `Escalation attempts are exhausted — this is the second round, and
|
|
17298
|
+
the check is still red. \`.gtd/ESCALATION.md\` holds the last
|
|
17299
|
+
unresolved analysis, and \`.gtd/FEEDBACK.md\` the last failing
|
|
17300
|
+
output.
|
|
17301
|
+
|
|
17302
|
+
Edit \`.gtd/ESCALATION.md\` with fresh instructions for the next fix
|
|
17303
|
+
turn, or land untouched to give it one more attempt at the same
|
|
17304
|
+
analysis.
|
|
17305
|
+
`;
|
|
17306
|
+
const buildContextPrompt = (built) => {
|
|
17307
|
+
const list = (paths) => paths.length === 0 ? " (none)" : paths.map((path) => ` - ${path}`).join("\n");
|
|
17308
|
+
return `- Read \`.gtd/ARCHITECTURE.md\` — the plan every package was built from
|
|
17309
|
+
- A red scenario that is NEW in this plan likely means missing wiring
|
|
17310
|
+
between packages; new scenarios:
|
|
17311
|
+
${list(built.scenarios.added)}
|
|
17312
|
+
- A red CHANGED or existing scenario or test is a regression a package
|
|
17313
|
+
introduced; scenarios this plan changed:
|
|
17314
|
+
${list(built.scenarios.changed)}
|
|
17315
|
+
- Each package's commits, to bisect a break:
|
|
17316
|
+
${built.ranges.map((r) => ` git log --oneline ${r.from}..${r.to} # ${r.pkg}`).join("\n")}
|
|
17317
|
+
`;
|
|
17790
17318
|
};
|
|
17791
|
-
const
|
|
17792
|
-
|
|
17793
|
-
|
|
17794
|
-
|
|
17795
|
-
|
|
17796
|
-
|
|
17797
|
-
|
|
17798
|
-
|
|
17799
|
-
|
|
17319
|
+
const buildFixPrompt = (built) => `${stateFileRules}
|
|
17320
|
+
${fixFeedbackPrompt}
|
|
17321
|
+
${built === void 0 ? "" : buildContextPrompt(built)}- Leave everything uncommitted — do not commit
|
|
17322
|
+
`;
|
|
17323
|
+
const finisherSystem = () => `${finisherPersona}
|
|
17324
|
+
|
|
17325
|
+
${agentConduct}`;
|
|
17326
|
+
const buildFixQualityPrompt = () => `${stateFileRules}
|
|
17327
|
+
- Read \`.gtd/QUALITY.md\` — one \`## \` chunk per finding a quality
|
|
17328
|
+
dimension wrote. Merge duplicate findings across dimensions
|
|
17329
|
+
FIRST, then fix every finding in every chunk, blocking or not
|
|
17330
|
+
- When findings conflict, missing test signal beats line count —
|
|
17331
|
+
a test is never deleted to satisfy a simplification finding
|
|
17332
|
+
- Delete \`.gtd/QUALITY.md\` once every finding is resolved
|
|
17333
|
+
- Leave everything else uncommitted and finish your turn
|
|
17334
|
+
`;
|
|
17335
|
+
/** Brief for a lens the workflow defines itself; a lens with none is just a skill of that name. */
|
|
17336
|
+
const correctnessBrief = `- Trace partial-failure and retry paths: what a step that fails
|
|
17337
|
+
after saving an id leaves behind, and whether the retry resumes
|
|
17338
|
+
it or starts over
|
|
17339
|
+
- Check validation done before an external side effect — its
|
|
17340
|
+
format, not just its presence
|
|
17341
|
+
- Check invariants that parallel write paths share: every path
|
|
17342
|
+
that writes the same data must enforce what the main path
|
|
17343
|
+
enforces
|
|
17344
|
+
- New mock behaviour without a contract test against the live
|
|
17345
|
+
behaviour (mock/live drift) is a finding`;
|
|
17346
|
+
const conventionsBrief = `- Read every \`AGENTS.md\` and \`CLAUDE.md\` in the repository root
|
|
17347
|
+
and in each touched file's directory ancestry, end to end, plus
|
|
17348
|
+
every file they pull in by \`@path\`
|
|
17349
|
+
- Every violation of them in the change is a finding — quote the
|
|
17350
|
+
rule it breaks`;
|
|
17351
|
+
const specChallengeBrief = `- Find this process's planning documents in history:
|
|
17352
|
+
\`git log <start>..HEAD\` (\`<start>\` is the commit above) over the steering directory
|
|
17353
|
+
(\`.gtd/\`), then \`git show\` the last version of the
|
|
17354
|
+
requirements, architecture and package files
|
|
17355
|
+
- Flag a spec decision that conflicts with a system invariant — an
|
|
17356
|
+
existing test, a documented constraint, or a data invariant
|
|
17357
|
+
other code relies on — naming the decision and the invariant
|
|
17358
|
+
- With no planning documents in history, write nothing`;
|
|
17359
|
+
const buildQualityReviewingPrompt = (lens, brief) => `${stateFileRules}
|
|
17360
|
+
- The only state file this turn writes is \`.gtd/QUALITY.md\` — no
|
|
17361
|
+
other files for notes or output
|
|
17362
|
+
- Review the whole assembled change, from \`${start()}\`
|
|
17363
|
+
to the working tree, through this ONE quality lens only —
|
|
17364
|
+
\`${lens}\`
|
|
17365
|
+
- Trace, do not skim: follow the order of external calls against
|
|
17366
|
+
the resume/retry logic. On a large change, read the touched code
|
|
17367
|
+
paths, not just the diff hunks
|
|
17368
|
+
- A test counts as coverage only if it would fail with the guarded
|
|
17369
|
+
behaviour removed — decide that by reasoning, never by running a
|
|
17370
|
+
mutation-testing tool. A test that pins a bug is a finding, not
|
|
17371
|
+
praise
|
|
17372
|
+
- APPEND every finding you have, blocking or not, as a \`## \`
|
|
17373
|
+
chunk to \`.gtd/QUALITY.md\` — never overwrite what an earlier
|
|
17374
|
+
dimension already wrote there
|
|
17375
|
+
- Write nothing only when this lens found nothing at all — then a
|
|
17376
|
+
clean turn IS this dimension's approval
|
|
17377
|
+
${brief ? `
|
|
17378
|
+
This lens's brief:
|
|
17379
|
+
${brief}
|
|
17380
|
+
|
|
17381
|
+
` : ""}- Touch no other state file, and leave everything uncommitted
|
|
17382
|
+
`;
|
|
17383
|
+
const reviewerSystem = () => `${reviewerPersona}
|
|
17384
|
+
|
|
17385
|
+
${agentConduct}`;
|
|
17386
|
+
const buildReviewAwaitReviewMessage = (base) => `\`.gtd/REVIEW.md\` holds the review record for the process — one
|
|
17387
|
+
\`- [ ]\` checkbox per reviewable item, grouped into chunks. Tick a box
|
|
17388
|
+
(\`- [x]\`) as you review each hunk; ticking only records that you've read
|
|
17389
|
+
it, it is not sign-off. Ticks are read-progress only: landing clears
|
|
17390
|
+
every box back to \`- [ ]\` on disk and nothing records which hunks
|
|
17391
|
+
you read — there is no persisted trail of it.
|
|
17392
|
+
|
|
17393
|
+
Review the diff yourself, with whatever tool you like — gtd checks
|
|
17394
|
+
nothing out and touches no ref; \`gtd base\` prints this same hash any
|
|
17395
|
+
time you need it again. The range runs from the review base to the
|
|
17396
|
+
working tree:
|
|
17397
|
+
|
|
17398
|
+
git diff ${base}
|
|
17399
|
+
When you've been through the whole diff, run \`gtd land\`:
|
|
17400
|
+
|
|
17401
|
+
- **Sign off** — leave no comment — no note in
|
|
17402
|
+
\`.gtd/REVIEW.md\`, no code edit — to close the process,
|
|
17403
|
+
whatever the boxes say. Every turn commit stays on the
|
|
17404
|
+
branch; run \`gtd summary\` afterward for a closing-message
|
|
17405
|
+
prompt.
|
|
17406
|
+
- **Ask a question** — start a thread (\`- H: <question>\`) on a
|
|
17407
|
+
footnote. The agent answers inside \`.gtd/REVIEW.md\` and the process
|
|
17408
|
+
rests at this same gate again — no revert, no development lap. A
|
|
17409
|
+
round that also leaves notes or edits folds those in the same turn
|
|
17410
|
+
and answers the thread.
|
|
17411
|
+
- **Leave notes** — a note on a \`.gtd/REVIEW.md\` line, a footnote
|
|
17412
|
+
anchored to a hunk, or prose under a chunk. Each note is judged on its
|
|
17413
|
+
own (**review.triage** gives one verdict per note):
|
|
17414
|
+
- \`edit\` — a change request: goes to **review.collecting** and a
|
|
17415
|
+
FULL development lap, re-planned from scratch
|
|
17416
|
+
- \`question\` — answered inline under the note in
|
|
17417
|
+
\`.gtd/REVIEW.md\` (**review.answer-review-questions**); the process
|
|
17418
|
+
rests at this gate again, no lap
|
|
17419
|
+
- \`nit\` — fixed in one batched turn (**review.fix.nits.fixing**), then a
|
|
17420
|
+
fresh review of the change rests at this gate again, no re-plan
|
|
17421
|
+
- \`praise\` — dropped; a round of only praise signs off
|
|
17422
|
+
When a round mixes \`edit\` with \`question\` or \`nit\`, questions get
|
|
17423
|
+
answered and nits fixed first, then the edits go to the lap. A note the
|
|
17424
|
+
judge is unsure about counts as \`edit\`.
|
|
17425
|
+
- **Edit code** — a direct code edit outside \`.gtd/\` goes straight to
|
|
17426
|
+
**review.collecting**, no verdict of your own required. A hand-edit
|
|
17427
|
+
you make here is treated as a SKETCH, not a fix the agent builds on: it
|
|
17428
|
+
is reverted out of the tree and re-planned from scratch, the same as any
|
|
17429
|
+
other change that starts a process.
|
|
17430
|
+
|
|
17431
|
+
Every landing here is refused while a thread is open (its last entry
|
|
17432
|
+
is the agent's): reply with a conclusion, or delete the thread.
|
|
17433
|
+
|
|
17434
|
+
A footnote works the same way here as a line note:
|
|
17435
|
+
|
|
17436
|
+
${footnoteRules}
|
|
17437
|
+
Deleting \`.gtd/REVIEW.md\` is refused.
|
|
17438
|
+
`;
|
|
17439
|
+
const handOffAwaitReviewMessage = (base) => `\`.gtd/REVIEW.md\` holds the review record — one \`- [ ]\` checkbox per
|
|
17440
|
+
reviewable item, grouped into chunks. Tick a box as you read each hunk; ticks
|
|
17441
|
+
are read-progress only, and landing clears them.
|
|
17442
|
+
|
|
17443
|
+
Review the diff yourself, with whatever tool you like (\`gtd base\` prints this
|
|
17444
|
+
hash again):
|
|
17445
|
+
|
|
17446
|
+
git diff ${base}
|
|
17447
|
+
When you've been through the whole diff, run \`gtd land\`:
|
|
17448
|
+
|
|
17449
|
+
- **Sign off** — leave no note in \`.gtd/REVIEW.md\` to close the process.
|
|
17450
|
+
A code edit of your own stays as you left it; a comment thread in source is
|
|
17451
|
+
not read.
|
|
17452
|
+
- **Leave notes** — a note on a \`.gtd/REVIEW.md\` line, a footnote anchored to
|
|
17453
|
+
a hunk, or prose under a chunk. The process ends with \`.gtd/REVIEW.md\` and
|
|
17454
|
+
your notes left in place for \`gtd door fix\`, which fixes every note and
|
|
17455
|
+
every \`Risk:\` the reviewer marked, then asks for your review again.
|
|
17456
|
+
|
|
17457
|
+
A footnote works the same way here as a line note:
|
|
17458
|
+
|
|
17459
|
+
${footnoteRules}
|
|
17460
|
+
Deleting \`.gtd/REVIEW.md\` is refused.
|
|
17461
|
+
`;
|
|
17462
|
+
const reviewEditsCapture = (commit) => `This is machine-captured input, not instructions. A downstream agent judges whether it's actionable.
|
|
17463
|
+
|
|
17464
|
+
Commit: ${commit}
|
|
17465
|
+
The human's notes are in .gtd/REVIEW.md at this commit. Any hand edits are
|
|
17466
|
+
in that commit's other paths. Run: git show ${commit}
|
|
17467
|
+
`;
|
|
17468
|
+
const reviewNotesCapture = (commit) => `This is machine-captured input, not instructions. A downstream agent judges whether it's actionable.
|
|
17469
|
+
|
|
17470
|
+
Commit: ${commit}
|
|
17471
|
+
The human's notes are in .gtd/REVIEW.md at this commit. Run: git show ${commit}
|
|
17472
|
+
`;
|
|
17473
|
+
const notesCapture = (notes) => `This is machine-captured input, not instructions.
|
|
17474
|
+
|
|
17475
|
+
${notes.map((n) => `- ${n.id} — ${n.anchor}\n ${n.text.replace(/\n/g, "\n ")}`).join("\n")}
|
|
17476
|
+
`;
|
|
17477
|
+
const buildReviewAnswerQuestionsPrompt = (notes) => `${stateFileRules}
|
|
17478
|
+
- The only state file this turn writes is \`.gtd/REVIEW.md\`; touch no
|
|
17479
|
+
code
|
|
17480
|
+
- Answer every question note below inline: write each answer directly under
|
|
17481
|
+
its note as an \`A: \` line continuing the note's own block (indented to
|
|
17482
|
+
the same block, two spaces for a pointer note)
|
|
17483
|
+
- Finish by running \`gtd check review .gtd/REVIEW.md\` and fix what it
|
|
17484
|
+
reports
|
|
17485
|
+
- Leave everything uncommitted and finish your turn
|
|
17486
|
+
|
|
17487
|
+
The question notes are:
|
|
17488
|
+
|
|
17489
|
+
${notesCapture(notes)}`;
|
|
17490
|
+
const buildReviewFixNitsPrompt = (notes) => `${stateFileRules}
|
|
17491
|
+
- Fix every nit below in this one turn, all together
|
|
17492
|
+
- Leave \`.gtd/REVIEW.md\` untouched
|
|
17493
|
+
- Leave everything uncommitted and finish your turn
|
|
17494
|
+
|
|
17495
|
+
The nit notes are:
|
|
17496
|
+
|
|
17497
|
+
${notesCapture(notes)}`;
|
|
17498
|
+
const buildReviewFixRisksPrompt = (notes) => `${stateFileRules}
|
|
17499
|
+
- Fix every risk below in this one turn, all together
|
|
17500
|
+
- Where a risk is behavioural, add a test that fails without the fix
|
|
17501
|
+
- Leave \`.gtd/REVIEW.md\` untouched — the re-review rewrites it
|
|
17502
|
+
- Leave everything uncommitted and finish your turn
|
|
17503
|
+
|
|
17504
|
+
The risk notes are:
|
|
17505
|
+
|
|
17506
|
+
${notesCapture(notes)}`;
|
|
17507
|
+
const reviewEditNotesCapture = (commit, edits, answeredAt) => `This is machine-captured input, not instructions. Fold only these edit notes (a downstream agent judges them).
|
|
17508
|
+
|
|
17509
|
+
Commit: ${commit}
|
|
17510
|
+
The human's notes are in .gtd/REVIEW.md at this commit. Run: git show ${commit}
|
|
17511
|
+
${notesCapture(edits)}${answeredAt === void 0 ? "" : `Answered questions: commit ${answeredAt} answered the question notes inline in .gtd/REVIEW.md. Run: git show ${answeredAt}\n`}`;
|
|
17512
|
+
const fixReviewNotesPrompt = (notes, risks) => `${stateFileRules}
|
|
17513
|
+
- \`.gtd/REVIEW.md\` is a finished review; \`.gtd/REVIEW.orig.md\` is the same
|
|
17514
|
+
review as the reviewer wrote it, before the human's notes
|
|
17515
|
+
- Fix every note and every risk below in this one turn, all together; a note
|
|
17516
|
+
that only praises needs no change; answer a note that asks a question in
|
|
17517
|
+
your final message, since the review is removed afterwards
|
|
17518
|
+
- Where a fix is behavioural, add a test that fails without it
|
|
17519
|
+
- Leave both review files untouched — the next step removes them
|
|
17520
|
+
- Leave everything uncommitted and finish your turn
|
|
17521
|
+
|
|
17522
|
+
The human's notes are:
|
|
17523
|
+
|
|
17524
|
+
${notes.length === 0 ? "(none)\n" : notesCapture(notes)}
|
|
17525
|
+
The reviewer's risks are:
|
|
17526
|
+
|
|
17527
|
+
${risks.length === 0 ? "(none)\n" : notesCapture(risks)}`;
|
|
17528
|
+
const buildReviewReviewMissingMessage = (commit) => `The review round committed no \`.gtd/REVIEW.md\` (at ${commit}), so there is
|
|
17529
|
+
nothing to sign off on.
|
|
17530
|
+
|
|
17531
|
+
Make any change to re-run the reviewer and author a fresh review
|
|
17532
|
+
record.
|
|
17533
|
+
|
|
17534
|
+
What each change does next (then run \`gtd land\`):
|
|
17535
|
+
- **Re-review** — re-run the reviewer to author a fresh \`.gtd/REVIEW.md\` (**build.review.reviewing**).
|
|
17536
|
+
`;
|
|
17537
|
+
const buildReviewTriageMessage = () => `Judging each note the human added to \`.gtd/REVIEW.md\` (a line note, a
|
|
17538
|
+
footnote, or prose under a chunk) with one verdict: \`edit\` (a change
|
|
17539
|
+
request), \`question\`, \`nit\` (a small fix needing no re-plan) or
|
|
17540
|
+
\`praise\`. Run \`gtd judge answer\` and pipe a choice per note — or land
|
|
17541
|
+
untouched, which treats every note as \`edit\` (the conservative
|
|
17542
|
+
default; a skipped judgment never dismisses a note).
|
|
17543
|
+
`;
|
|
17544
|
+
const buildReviewCollectingPrompt = (capture) => `${styleBlock}
|
|
17545
|
+
|
|
17546
|
+
${styleFormatContract}
|
|
17547
|
+
|
|
17548
|
+
You are judging and classifying a round of review feedback.
|
|
17549
|
+
|
|
17550
|
+
${stateFileRules}
|
|
17551
|
+
${footnoteFoldIn}${codeThreadReplies()}
|
|
17552
|
+
- This turn writes \`.gtd/REQUIREMENTS.md\` (the folded concerns) and
|
|
17553
|
+
replies inside \`.gtd/REVIEW.md\` (thread replies only; the file stays
|
|
17554
|
+
in the tree) — you classify, you do not build
|
|
17555
|
+
- Fold every concluded thread, ticked answer and hand-edit — and each
|
|
17556
|
+
\`edit\` note the capture lists, no other note — into
|
|
17557
|
+
\`.gtd/REQUIREMENTS.md\` and delete the folded threads from
|
|
17558
|
+
\`.gtd/REVIEW.md\`; append one \`- A:\` reply to each thread
|
|
17559
|
+
whose last entry is a \`- H:\` question
|
|
17560
|
+
- When the capture names an answering commit, carry each answered
|
|
17561
|
+
question into \`.gtd/REQUIREMENTS.md\`'s \`## Answered Questions\` as
|
|
17562
|
+
\`### <the note>\` plus its answer — the lap replaces \`.gtd/REVIEW.md\`
|
|
17563
|
+
- Nit notes were already fixed: never re-raise a fixed nit as a concern
|
|
17564
|
+
- Finish by running \`gtd check review .gtd/REVIEW.md\` and fix
|
|
17565
|
+
what it reports
|
|
17566
|
+
|
|
17567
|
+
The raw review material is:
|
|
17568
|
+
|
|
17569
|
+
${capture}
|
|
17570
|
+
It names a commit. Work from what you already reviewed if you
|
|
17571
|
+
wrote today's review earlier this conversation; otherwise read
|
|
17572
|
+
that commit's diff yourself first.
|
|
17573
|
+
|
|
17574
|
+
The round is actionable if any of these hold:
|
|
17575
|
+
|
|
17576
|
+
- The human left a note on \`.gtd/REVIEW.md\`. A note is a mandatory
|
|
17577
|
+
concern below
|
|
17578
|
+
- The human added a code comment this round. A comment run whose
|
|
17579
|
+
first line starts \`H:\` is a THREAD: when its last entry is an
|
|
17580
|
+
\`H:\` question, write exactly one \`A:\` comment line, same token
|
|
17581
|
+
and indentation, directly below it — no fold-in; a concluded
|
|
17582
|
+
thread is folded into the requirements and its comment lines
|
|
17583
|
+
deleted. Any other comment, even a plain-prose one, is a one-shot
|
|
17584
|
+
concern — describe it, and note the comment line itself is
|
|
17585
|
+
transient: it must not survive the lap that satisfies it
|
|
17586
|
+
- The human hand-edited non-comment code this round — no longer
|
|
17587
|
+
a committed intent to build on, but a sketch like the entry
|
|
17588
|
+
commit's own diff. Describe what it was reaching for; expect
|
|
17589
|
+
the next lap to re-derive it from scratch, never call it final
|
|
17590
|
+
|
|
17591
|
+
Not actionable only when none of the above holds — nothing but an
|
|
17592
|
+
approving remark, no code edit, no substantive note. Never invent
|
|
17593
|
+
actionability, and never dismiss a real note or edit as approval.
|
|
17594
|
+
|
|
17595
|
+
- If actionable: write \`.gtd/REQUIREMENTS.md\` with an ordered
|
|
17596
|
+
list of concerns, each PRODUCT or TECHNICAL — the shape
|
|
17597
|
+
\`design.triage\` builds, one \`## <heading>\` per concern in
|
|
17598
|
+
build order. Fold every note, comment, and hand-edit in under
|
|
17599
|
+
its concern. Raise no open questions here — \`design.triage\`
|
|
17600
|
+
owns that later. Then finish
|
|
17601
|
+
- If not: finish without writing anything — changing nothing is
|
|
17602
|
+
the sign-off
|
|
17603
|
+
`;
|
|
17604
|
+
const buildReviewReviewingPrompt = (base, carry) => `${styleBlock}
|
|
17605
|
+
|
|
17606
|
+
${styleFormatContract}
|
|
17607
|
+
|
|
17608
|
+
${stateFileRules}
|
|
17609
|
+
- The only state file this turn touches is \`.gtd/REVIEW.md\` —
|
|
17610
|
+
no other files for notes or output
|
|
17611
|
+
|
|
17612
|
+
Write \`.gtd/REVIEW.md\` in this exact format, to help a human
|
|
17613
|
+
review the changes:
|
|
17614
|
+
|
|
17615
|
+
- First non-blank line: \`# Review: ${head$1().slice(0, 7)}\`
|
|
17616
|
+
- Somewhere in the document: \`<!-- base: ${base} -->\`
|
|
17617
|
+
- At least one \`## <Chunk Title>\` heading grouping hunks
|
|
17618
|
+
semantically (same feature/refactor/fix, across files), each
|
|
17619
|
+
with a short explanation of what changed and why, then one
|
|
17620
|
+
pointer per hunk (\`./\`-relative path, mandatory \`#start-end\`;
|
|
17621
|
+
checkboxes are for the human, not you). A pointer into a NEW file
|
|
17622
|
+
gets a range covering only the region the note is about — never
|
|
17623
|
+
the whole file. Put the note's opening line right on the
|
|
17624
|
+
pointer's line:
|
|
17625
|
+
|
|
17626
|
+
- [ ] ./path/to/file.ts#42-70 — what this hunk does
|
|
17627
|
+
|
|
17628
|
+
Continue a longer note below the pointer, indented exactly two spaces
|
|
17629
|
+
— never four or more, which reads as a code block and never
|
|
17630
|
+
reflows:
|
|
17631
|
+
|
|
17632
|
+
- [ ] ./path/to/file.ts#42-70 — what this hunk does
|
|
17633
|
+
and here is more detail, continued below it
|
|
17634
|
+
|
|
17635
|
+
Open a hunk's note with \`Risk:\` only for a concrete defect
|
|
17636
|
+
the change introduces, never a style remark — each such note is
|
|
17637
|
+
fixed automatically before the human sees the review. On the
|
|
17638
|
+
re-review after a fix, describe what the fix changed under the
|
|
17639
|
+
hunk it touched, and re-mark only a risk the fix did not resolve:
|
|
17640
|
+
|
|
17641
|
+
- [ ] ./path/to/file.ts#42-70 — Risk: what is wrong
|
|
17642
|
+
|
|
17643
|
+
A note sitting entirely on the line(s) beneath the pointer is
|
|
17644
|
+
also valid. Either way, the note must never start with a bare \`./path\` token
|
|
17645
|
+
— that parses as a second pointer, not a note
|
|
17646
|
+
No diff is given — read the changes yourself. The range runs
|
|
17647
|
+
from \`${base}\` to the working tree (committed turns
|
|
17648
|
+
plus anything pending); on a feedback round that's the previous
|
|
17649
|
+
review's boundary, so it covers only what's new.
|
|
17650
|
+
|
|
17651
|
+
${carry === void 0 ? "" : `Carry-over: commit \`${carry}\` answered questions inline in the previous
|
|
17652
|
+
review (each \`A: \` line under the note it answers). Copy each answered
|
|
17653
|
+
note and its \`A: \` answer, verbatim, under the matching hunk of this fresh
|
|
17654
|
+
review. Run: git show ${carry}
|
|
17655
|
+
|
|
17656
|
+
`}Leave \`.gtd/REVIEW.md\` uncommitted and finish.
|
|
17657
|
+
`;
|
|
17658
|
+
/** `gtd summary`'s prompt: printed cold, no session identity, no diff inlined. */
|
|
17659
|
+
const summaryPrompt = (it) => {
|
|
17660
|
+
const human = it.humanCommits.length > 0 ? `The human contributed at these commits,
|
|
17661
|
+
oldest to newest:
|
|
17662
|
+
${it.humanCommits.map((c) => `- \`${c.hash}\` (entering \`${c.state}\`)\n`).join("")}
|
|
17663
|
+
` : `The human left no comment or edit this process —
|
|
17664
|
+
every commit is machine-authored.
|
|
17665
|
+
`;
|
|
17666
|
+
return `${styleBlock}
|
|
17667
|
+
|
|
17668
|
+
- Write the closing message for the process HEAD closes or sits inside —
|
|
17669
|
+
for a squash, an amend, or a PR body. Starting cold: read every
|
|
17670
|
+
decision out of the commits below, not assumed context
|
|
17671
|
+
- Cover the motivation, the decisions, the trade-offs, and the
|
|
17672
|
+
high-level architectural changes — never which files changed; \`git
|
|
17673
|
+
diff --stat\` is for that, not this message
|
|
17674
|
+
- If the range removes or renames a documented config key, CLI flag, or
|
|
17675
|
+
workflow state, OR makes a previously-accepted config value now fail to
|
|
17676
|
+
load (a stricter validation, an escape a value must now spell differently)
|
|
17677
|
+
— a public surface, not an internal one, either way — add a literal
|
|
17678
|
+
\`BREAKING CHANGE:\` footer naming what breaks and, for the second case,
|
|
17679
|
+
the exact escape or rewrite a committed config needs. A \`!\` on the
|
|
17680
|
+
type prefix alone (\`feat!:\`/\`refactor!:\`) is NOT enough: this repo's
|
|
17681
|
+
own commit-analyzer has missed that marker before, cutting no release
|
|
17682
|
+
until a follow-up commit carried the literal footer instead
|
|
17683
|
+
|
|
17684
|
+
The process's entry commit is \`${it.entryCommit}\`. ${human}Inspect the range: \`git log ${it.processBase}..${it.processTip}\`
|
|
17685
|
+
and \`git diff ${it.processBase} ${it.processTip}\` — exactly what
|
|
17686
|
+
a squash or PR body should describe.
|
|
17687
|
+
|
|
17688
|
+
Token cost: ${it.processCost}
|
|
17689
|
+
${it.processCostByModel.map((m) => `- ${m.model}: ${m.cost}\n`).join("")}
|
|
17690
|
+
Print the closing message and stop — this writes nothing itself.
|
|
17691
|
+
`;
|
|
17800
17692
|
};
|
|
17801
|
-
|
|
17693
|
+
const scenarioWordingMessage = (at, drifted) => `The e2e scenario wording was frozen when package 0 landed, and the last
|
|
17694
|
+
turn changed it. Removed lines are marked \`-\`, added ones \`+\`:
|
|
17695
|
+
|
|
17696
|
+
${drifted.map(({ path, lines }) => `${path}\n${lines.join("\n")}`).join("\n\n")}
|
|
17697
|
+
|
|
17698
|
+
To accept the change, land untouched.
|
|
17699
|
+
|
|
17700
|
+
To reject it, run
|
|
17701
|
+
|
|
17702
|
+
git checkout ${at} -- ${drifted.map(({ path }) => quote(path)).join(" ")}
|
|
17703
|
+
|
|
17704
|
+
and land. Restoring only some of the files is a partial accept: every file
|
|
17705
|
+
still differing from the frozen wording is accepted.
|
|
17706
|
+
`;
|
|
17802
17707
|
//#endregion
|
|
17803
|
-
//#region src/workflows/
|
|
17804
|
-
|
|
17805
|
-
const
|
|
17806
|
-
|
|
17807
|
-
|
|
17808
|
-
|
|
17809
|
-
|
|
17810
|
-
|
|
17811
|
-
const
|
|
17812
|
-
|
|
17813
|
-
|
|
17814
|
-
|
|
17815
|
-
|
|
17816
|
-
|
|
17817
|
-
|
|
17818
|
-
|
|
17819
|
-
|
|
17708
|
+
//#region src/workflows/shared/steps.ts
|
|
17709
|
+
const FEEDBACK = ".gtd/FEEDBACK.md";
|
|
17710
|
+
const ESCALATION = ".gtd/ESCALATION.md";
|
|
17711
|
+
const REQUIREMENTS = ".gtd/REQUIREMENTS.md";
|
|
17712
|
+
const ARCHITECTURE = ".gtd/ARCHITECTURE.md";
|
|
17713
|
+
const REVIEW = ".gtd/REVIEW.md";
|
|
17714
|
+
const QUALITY = ".gtd/QUALITY.md";
|
|
17715
|
+
const planner = () => env.plannerModel ?? "";
|
|
17716
|
+
const coder = () => env.coderModel ?? "";
|
|
17717
|
+
const fixCheck = (built) => agentWithSkills("fix", buildFixPrompt(built), {
|
|
17718
|
+
label: "Fixing the check",
|
|
17719
|
+
file: FEEDBACK,
|
|
17720
|
+
model: coder(),
|
|
17721
|
+
system: finisherSystem()
|
|
17722
|
+
});
|
|
17723
|
+
const describeEscalation = () => agentWithSkills("health.describe", healthDescribePrompt(), {
|
|
17724
|
+
label: "Describing the escalation",
|
|
17725
|
+
file: FEEDBACK,
|
|
17726
|
+
model: coder(),
|
|
17727
|
+
system: escalationSystem(),
|
|
17728
|
+
allowEmpty: true
|
|
17729
|
+
});
|
|
17730
|
+
const escalate = () => human("health.stop", {
|
|
17731
|
+
message: healthStopMessage(),
|
|
17732
|
+
label: "Escalating to a human",
|
|
17733
|
+
file: ESCALATION,
|
|
17734
|
+
acceptClean: true
|
|
17735
|
+
});
|
|
17736
|
+
const escalationExhausted = () => human("health.exhausted", {
|
|
17737
|
+
message: healthExhaustedMessage(),
|
|
17738
|
+
label: "Escalation exhausted",
|
|
17739
|
+
file: ESCALATION,
|
|
17740
|
+
acceptClean: true
|
|
17741
|
+
});
|
|
17742
|
+
/** Lenses the workflow defines itself, not bundled skills: `skills/` is not in the npm package, and a missing lens skill burns a turn silently. */
|
|
17743
|
+
const builtInLenses = {
|
|
17744
|
+
correctness: {
|
|
17745
|
+
skills: ["code-review-and-quality"],
|
|
17746
|
+
brief: correctnessBrief
|
|
17747
|
+
},
|
|
17748
|
+
conventions: {
|
|
17749
|
+
skills: [],
|
|
17750
|
+
brief: conventionsBrief
|
|
17751
|
+
},
|
|
17752
|
+
"spec-challenge": {
|
|
17753
|
+
skills: [],
|
|
17754
|
+
brief: specChallengeBrief
|
|
17820
17755
|
}
|
|
17821
|
-
await escalation(escalations);
|
|
17822
|
-
return false;
|
|
17823
17756
|
};
|
|
17824
|
-
|
|
17825
|
-
|
|
17826
|
-
"
|
|
17827
|
-
|
|
17828
|
-
|
|
17829
|
-
|
|
17830
|
-
|
|
17831
|
-
const verdictQuestion = (note) => ({
|
|
17832
|
-
id: note.id,
|
|
17833
|
-
primitive: "choice",
|
|
17834
|
-
instructions: `Classify the human's note on review item "${note.anchor}" (in .gtd/REVIEW.md). Its evidence holds the anchor, the reviewer's text and the human's text.`,
|
|
17835
|
-
criteria: "edit: a request to change behaviour, design or scope — anything needing a plan, or any note you are unsure about. question: asks something and wants an answer, with no change requested. nit: a small, local, unambiguous fix (naming, typo, formatting, comment wording) needing no re-plan. praise: an approving remark with nothing to do."
|
|
17757
|
+
/** One quality review through `lens`; each lens is its own scope, so its own conversation and skills. */
|
|
17758
|
+
const reviewQuality = (lens) => agentWithSkills(`quality.${lens}.reviewing`, buildQualityReviewingPrompt(lens, builtInLenses[lens]?.brief), {
|
|
17759
|
+
label: "Reviewing (one quality lens)",
|
|
17760
|
+
file: QUALITY,
|
|
17761
|
+
model: planner(),
|
|
17762
|
+
system: reviewerSystem(),
|
|
17763
|
+
allowEmpty: true
|
|
17836
17764
|
});
|
|
17837
|
-
|
|
17838
|
-
|
|
17839
|
-
|
|
17840
|
-
|
|
17841
|
-
|
|
17842
|
-
|
|
17843
|
-
|
|
17844
|
-
|
|
17845
|
-
|
|
17846
|
-
|
|
17847
|
-
|
|
17848
|
-
|
|
17849
|
-
|
|
17850
|
-
|
|
17851
|
-
|
|
17852
|
-
|
|
17853
|
-
|
|
17854
|
-
|
|
17855
|
-
|
|
17856
|
-
|
|
17857
|
-
|
|
17858
|
-
|
|
17859
|
-
|
|
17860
|
-
|
|
17861
|
-
|
|
17862
|
-
|
|
17863
|
-
|
|
17864
|
-
|
|
17865
|
-
|
|
17866
|
-
|
|
17867
|
-
|
|
17868
|
-
|
|
17869
|
-
|
|
17870
|
-
|
|
17871
|
-
|
|
17872
|
-
|
|
17873
|
-
|
|
17874
|
-
|
|
17875
|
-
|
|
17876
|
-
|
|
17877
|
-
|
|
17878
|
-
|
|
17879
|
-
|
|
17880
|
-
|
|
17881
|
-
|
|
17882
|
-
|
|
17883
|
-
|
|
17884
|
-
|
|
17885
|
-
|
|
17886
|
-
|
|
17887
|
-
|
|
17888
|
-
|
|
17889
|
-
|
|
17890
|
-
|
|
17765
|
+
const fixQuality = () => agentWithSkills("fix.quality.fixing", buildFixQualityPrompt(), {
|
|
17766
|
+
label: "Fixing quality findings",
|
|
17767
|
+
file: QUALITY,
|
|
17768
|
+
model: coder(),
|
|
17769
|
+
system: finisherSystem(),
|
|
17770
|
+
allowEmpty: true
|
|
17771
|
+
});
|
|
17772
|
+
/** Write `.gtd/REVIEW.md` over everything since `base`, carrying the answers of commit `carry` when given. */
|
|
17773
|
+
const reviewing = (base, carry) => agentWithSkills("review.reviewing", buildReviewReviewingPrompt(base, carry), {
|
|
17774
|
+
label: "Reviewing",
|
|
17775
|
+
file: REVIEW,
|
|
17776
|
+
mode: "review",
|
|
17777
|
+
model: planner(),
|
|
17778
|
+
system: reviewerSystem(),
|
|
17779
|
+
base
|
|
17780
|
+
});
|
|
17781
|
+
/** Fix the `Risk:`-marked notes the reviewer named; an empty turn means the risk was judged false. */
|
|
17782
|
+
const fixRisks = (notes) => agentWithSkills("review.fix.risks.fixing", buildReviewFixRisksPrompt(notes), {
|
|
17783
|
+
label: "Fixing the reviewer's risks",
|
|
17784
|
+
file: REVIEW,
|
|
17785
|
+
model: planner(),
|
|
17786
|
+
system: reviewerSystem(),
|
|
17787
|
+
allowEmpty: true
|
|
17788
|
+
});
|
|
17789
|
+
const awaitReview = (base, message = buildReviewAwaitReviewMessage(base)) => human("review.await-review", {
|
|
17790
|
+
message,
|
|
17791
|
+
label: "Awaiting your review",
|
|
17792
|
+
file: REVIEW,
|
|
17793
|
+
mode: "review",
|
|
17794
|
+
base
|
|
17795
|
+
});
|
|
17796
|
+
/** The round at commit `round` left no `.gtd/REVIEW.md`. */
|
|
17797
|
+
const reviewMissing = (round) => human("review.review-missing", {
|
|
17798
|
+
message: buildReviewReviewMissingMessage(round),
|
|
17799
|
+
label: "Nothing to review"
|
|
17800
|
+
});
|
|
17801
|
+
//#endregion
|
|
17802
|
+
//#region src/workflows/shared/health.ts
|
|
17803
|
+
/** Written while `fastTestCommand` is unset; its presence rests the process at the check. */
|
|
17804
|
+
const SETUP = ".gtd/SETUP.md";
|
|
17805
|
+
const SETUP_TEXT = "The bundled workflow needs the `fastTestCommand` setting: the fast suite (everything but e2e).\nSet it as a workflow option in `gtd.config.ts` or as GTD_FASTTESTCOMMAND.";
|
|
17806
|
+
/** Shell lines for a check's preamble: unset (blank after trim) setting writes `SETUP` and skips the suite. */
|
|
17807
|
+
const fastSuiteGuard = () => (env.fastTestCommand ?? "").trim() === "" ? [
|
|
17808
|
+
`mkdir -p ${quote(dirname(SETUP))}`,
|
|
17809
|
+
`printf '%s\\n' ${quote(SETUP_TEXT)} > ${quote(SETUP)}`,
|
|
17810
|
+
`printf '%s\\n' ${quote(SETUP_TEXT)} >&2`,
|
|
17811
|
+
"exit 0"
|
|
17812
|
+
] : [`rm -f ${quote(SETUP)}`];
|
|
17813
|
+
/** Fix turns a run of red checks gets before it escalates. */
|
|
17814
|
+
const FIX_CAP = 3;
|
|
17815
|
+
const unconfigured = () => read(SETUP) !== void 0;
|
|
17816
|
+
/** Run the suite as step `name`; resolves `true` when it passed. A failure is in `.gtd/FEEDBACK.md`. */
|
|
17817
|
+
const baseline = (name, label = "Checking the baseline") => untilSettled(() => check(name, env.testCommand ?? "", {
|
|
17818
|
+
report: FEEDBACK,
|
|
17819
|
+
label,
|
|
17820
|
+
preamble: fastSuiteGuard()
|
|
17821
|
+
}), unconfigured);
|
|
17822
|
+
const escalationSteps = {
|
|
17823
|
+
limit: 2,
|
|
17824
|
+
describe: describeEscalation,
|
|
17825
|
+
stop: escalate,
|
|
17826
|
+
exhausted: escalationExhausted
|
|
17891
17827
|
};
|
|
17892
17828
|
/**
|
|
17893
|
-
*
|
|
17894
|
-
*
|
|
17895
|
-
*
|
|
17829
|
+
* Hand a red suite that repeated attempts could not fix to a person: an
|
|
17830
|
+
* agent writes `.gtd/ESCALATION.md`, a human narrows it. From the third round
|
|
17831
|
+
* on, the human is told the budget is spent instead.
|
|
17896
17832
|
*/
|
|
17897
|
-
const
|
|
17898
|
-
|
|
17899
|
-
|
|
17900
|
-
|
|
17901
|
-
|
|
17902
|
-
|
|
17903
|
-
|
|
17904
|
-
}
|
|
17905
|
-
|
|
17906
|
-
|
|
17907
|
-
|
|
17908
|
-
|
|
17909
|
-
const [edits, questions, nits] = [
|
|
17910
|
-
of("edit"),
|
|
17911
|
-
of("question"),
|
|
17912
|
-
of("nit")
|
|
17913
|
-
];
|
|
17914
|
-
if (edits.length + questions.length + nits.length === 0) return r.unfolded();
|
|
17915
|
-
let answeredAt;
|
|
17916
|
-
if (questions.length > 0) {
|
|
17917
|
-
await answerReviewQuestions(questions);
|
|
17918
|
-
answeredAt = head$1();
|
|
17919
|
-
}
|
|
17920
|
-
if (nits.length > 0) {
|
|
17921
|
-
await guarded(r.frozen, () => fixNits(nits), "review")();
|
|
17922
|
-
await healthy(guarded(r.frozen, fixCheck, "review"), { escalations: r.escalations });
|
|
17923
|
-
}
|
|
17924
|
-
if (edits.length > 0) {
|
|
17925
|
-
await r.close();
|
|
17926
|
-
return r.outcome(await collect(reviewEditNotesCapture(r.round, edits, answeredAt)));
|
|
17927
|
-
}
|
|
17928
|
-
if (nits.length > 0) {
|
|
17929
|
-
await r.close();
|
|
17930
|
-
return {
|
|
17931
|
-
next: "rereview",
|
|
17932
|
-
carry: answeredAt
|
|
17933
|
-
};
|
|
17934
|
-
}
|
|
17935
|
-
return {
|
|
17936
|
-
next: "await",
|
|
17937
|
-
reviewed: answeredAt
|
|
17938
|
-
};
|
|
17833
|
+
const escalation = (count) => escalation$1(count, escalationSteps);
|
|
17834
|
+
/** "Is this red round the same failure as the last one?" — asked of two reports, or of nothing when one is empty. */
|
|
17835
|
+
const retryQuestion = (comparable) => comparable ? {
|
|
17836
|
+
id: "verdict",
|
|
17837
|
+
primitive: "choice",
|
|
17838
|
+
instructions: "Compare this round's failing check output (current) against the previous round's (previous), both in state. Classify the change.",
|
|
17839
|
+
criteria: "identical: the same failure restated, byte-for-byte or the same root cause — the trailing `<!-- gtd check <sha> -->` stamp is already stripped from both, but ignore it too if it ever reappears. new-failure: a materially different symptom than previous. progress: still red, but measurably closer to green (fewer failures, a later stage reached)."
|
|
17840
|
+
} : {
|
|
17841
|
+
id: "verdict",
|
|
17842
|
+
primitive: "choice",
|
|
17843
|
+
instructions: "state carries tailsNotComparable: one of the two check reports came back empty. There is nothing to compare here; answer from criteria alone. `identical` is FORBIDDEN here.",
|
|
17844
|
+
criteria: "new-failure: a materially different failure than before. progress: still red, but closer to green — the default if genuinely unsure."
|
|
17939
17845
|
};
|
|
17940
|
-
const
|
|
17941
|
-
const
|
|
17942
|
-
const
|
|
17943
|
-
|
|
17944
|
-
|
|
17945
|
-
|
|
17946
|
-
|
|
17947
|
-
|
|
17948
|
-
|
|
17949
|
-
label: "
|
|
17950
|
-
base: round
|
|
17951
|
-
});
|
|
17952
|
-
const outcome = (verdict) => verdict === "signoff" ? { verdict } : {
|
|
17953
|
-
verdict,
|
|
17954
|
-
base: round,
|
|
17955
|
-
restoreFrom: reviewed,
|
|
17956
|
-
edited
|
|
17957
|
-
};
|
|
17958
|
-
const unfolded = async () => {
|
|
17959
|
-
await close();
|
|
17960
|
-
return outcome(folded ? "feedback" : "signoff");
|
|
17961
|
-
};
|
|
17962
|
-
const collectWith = async (capture) => {
|
|
17963
|
-
await close();
|
|
17964
|
-
return outcome(await collect(capture));
|
|
17965
|
-
};
|
|
17966
|
-
if (settled || edited.length === 0 && !noted) return unfolded();
|
|
17967
|
-
if (edited.length > 0) return collectWith(reviewEditsCapture(round));
|
|
17968
|
-
const notes = reviewNotes(baselineText, record);
|
|
17969
|
-
if (notes.length === 0) return collectWith(reviewNotesCapture(round));
|
|
17970
|
-
return routeNotes(notes, {
|
|
17971
|
-
round,
|
|
17972
|
-
escalations,
|
|
17973
|
-
frozen,
|
|
17974
|
-
close,
|
|
17975
|
-
outcome,
|
|
17976
|
-
unfolded
|
|
17846
|
+
const sameFailure = async (previous, current) => {
|
|
17847
|
+
const comparable = current.trim() !== "" && previous.trim() !== "";
|
|
17848
|
+
const { answers, truncated } = await judge("health.judge", {
|
|
17849
|
+
questions: [retryQuestion(comparable)],
|
|
17850
|
+
evidence: comparable ? {
|
|
17851
|
+
current,
|
|
17852
|
+
previous
|
|
17853
|
+
} : { tailsNotComparable: "true" },
|
|
17854
|
+
message: healthJudgeMessage(),
|
|
17855
|
+
label: "Judging the retry"
|
|
17977
17856
|
});
|
|
17857
|
+
return comparable && truncated.length === 0 && answered(answers.verdict, "identical", numeric(vars.judgeIdenticalMinP, Infinity));
|
|
17978
17858
|
};
|
|
17979
|
-
|
|
17980
|
-
const
|
|
17981
|
-
|
|
17982
|
-
|
|
17983
|
-
|
|
17984
|
-
|
|
17985
|
-
|
|
17986
|
-
|
|
17859
|
+
const runSuite = (options) => {
|
|
17860
|
+
const fast = options.suite === "fast";
|
|
17861
|
+
return check("health.check", (fast ? env.fastTestCommand : env.testCommand) ?? "", {
|
|
17862
|
+
report: FEEDBACK,
|
|
17863
|
+
label: "Running checks",
|
|
17864
|
+
sweepOnGreen: [ESCALATION, ...options.sweepOnGreen ?? []],
|
|
17865
|
+
...fast ? { preamble: fastSuiteGuard() } : {}
|
|
17866
|
+
});
|
|
17987
17867
|
};
|
|
17988
17868
|
/**
|
|
17989
|
-
*
|
|
17990
|
-
*
|
|
17991
|
-
*
|
|
17992
|
-
* batch, praise dropped.
|
|
17869
|
+
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
17870
|
+
* first once `FIX_CAP` fixes are spent, or a judge calls the failure
|
|
17871
|
+
* identical to the previous round's.
|
|
17993
17872
|
*/
|
|
17994
|
-
const
|
|
17995
|
-
|
|
17996
|
-
|
|
17997
|
-
|
|
17998
|
-
|
|
17999
|
-
|
|
18000
|
-
|
|
18001
|
-
|
|
18002
|
-
collectedAt = await converse(base, collectedAt);
|
|
18003
|
-
if (collectedAt === "missing") break;
|
|
18004
|
-
const result = await finish(reviewed, collectedAt, escalations, frozen);
|
|
18005
|
-
if (!("next" in result)) return result;
|
|
18006
|
-
if (result.next === "rereview") {
|
|
18007
|
-
carry = result.carry;
|
|
18008
|
-
break;
|
|
18009
|
-
}
|
|
18010
|
-
reviewed = result.reviewed;
|
|
18011
|
-
}
|
|
18012
|
-
}
|
|
18013
|
-
};
|
|
18014
|
-
/** The build tail: fix (when entered red), keep green, the quality lap, then human review since `base`. */
|
|
18015
|
-
const buildTail = (fixFirst, base, built) => scope("build", async () => {
|
|
18016
|
-
const frozen = built?.frozen;
|
|
18017
|
-
const escalations = { rounds: 0 };
|
|
18018
|
-
const guardedFix = guarded(frozen, () => fixCheck());
|
|
18019
|
-
let redFirst = fixFirst;
|
|
18020
|
-
if (!fixFirst) await healthy(guarded(frozen, () => fixCheck(built)), {
|
|
18021
|
-
escalations,
|
|
18022
|
-
sweepOnGreen: [ARCHITECTURE]
|
|
18023
|
-
});
|
|
18024
|
-
let lapped = false;
|
|
18025
|
-
for (;;) {
|
|
18026
|
-
if (redFirst) {
|
|
18027
|
-
await guardedFix();
|
|
18028
|
-
await healthy(guardedFix, {
|
|
18029
|
-
fixesSoFar: 1,
|
|
18030
|
-
escalations
|
|
18031
|
-
});
|
|
18032
|
-
redFirst = false;
|
|
18033
|
-
}
|
|
18034
|
-
const lap = lapped ? "clean" : await qualityLap();
|
|
18035
|
-
lapped = true;
|
|
18036
|
-
if (lap === "clean") return review$1(base, escalations, frozen);
|
|
18037
|
-
if (await fixQualityFindings(escalations, frozen)) await healthy(guardedFix, { escalations });
|
|
18038
|
-
else redFirst = true;
|
|
18039
|
-
}
|
|
17873
|
+
const healthy = (fix, options = {}) => healthy$1(fix, {
|
|
17874
|
+
check: options.suite === "fast" ? () => untilSettled(() => runSuite(options), unconfigured) : () => runSuite(options),
|
|
17875
|
+
report: FEEDBACK,
|
|
17876
|
+
fixCap: 3,
|
|
17877
|
+
fixesSoFar: options.fixesSoFar ?? 0,
|
|
17878
|
+
sameFailure,
|
|
17879
|
+
escalations: options.escalations ?? { rounds: 0 },
|
|
17880
|
+
escalation: escalationSteps
|
|
18040
17881
|
});
|
|
17882
|
+
/** Hold the process at `<name>.blocked` until the suite is green. */
|
|
17883
|
+
const gate = (name, message) => gate$1(name, () => baseline("check"), () => human("blocked", {
|
|
17884
|
+
message,
|
|
17885
|
+
label: "Baseline is red",
|
|
17886
|
+
file: FEEDBACK
|
|
17887
|
+
}));
|
|
18041
17888
|
//#endregion
|
|
18042
17889
|
//#region node_modules/effect/dist/esm/Function.js
|
|
18043
17890
|
/**
|
|
@@ -37353,7 +37200,7 @@ Struct({
|
|
|
37353
37200
|
description: "Represents an issue returned by the ArrayFormatter formatter"
|
|
37354
37201
|
});
|
|
37355
37202
|
//#endregion
|
|
37356
|
-
//#region src/workflows/vars.ts
|
|
37203
|
+
//#region src/workflows/shared/vars.ts
|
|
37357
37204
|
/** Process settings. */
|
|
37358
37205
|
const defaults = {
|
|
37359
37206
|
reviewBase: "",
|
|
@@ -37370,7 +37217,7 @@ const envDefaults = {
|
|
|
37370
37217
|
coderModel: "base"
|
|
37371
37218
|
};
|
|
37372
37219
|
//#endregion
|
|
37373
|
-
//#region src/workflows/options.ts
|
|
37220
|
+
//#region src/workflows/shared/options.ts
|
|
37374
37221
|
const setting = optionalWith(String$, { exact: true });
|
|
37375
37222
|
const ShippedOptions = Struct({
|
|
37376
37223
|
reviewBase: setting,
|
|
@@ -37388,48 +37235,355 @@ const over = (base, options) => ({
|
|
|
37388
37235
|
...Object.fromEntries(Object.entries(options).filter(([name]) => Object.hasOwn(base, name)))
|
|
37389
37236
|
});
|
|
37390
37237
|
/** A shipped flow as a factory: its options override the settings it declares, each in its own kind. */
|
|
37391
|
-
const shipped = (flow) => defineWorkflow(standardSchemaV1(ShippedOptions, {
|
|
37238
|
+
const shipped = (flow, doors, base) => defineWorkflow(standardSchemaV1(ShippedOptions, {
|
|
37392
37239
|
onExcessProperty: "error",
|
|
37393
37240
|
errors: "all"
|
|
37394
37241
|
}), (options) => ({
|
|
37395
37242
|
flow,
|
|
37396
37243
|
defaults: over(defaults, options),
|
|
37397
|
-
envDefaults: over(envDefaults, options)
|
|
37244
|
+
envDefaults: over(envDefaults, options),
|
|
37245
|
+
doors,
|
|
37246
|
+
base
|
|
37398
37247
|
}));
|
|
37399
37248
|
//#endregion
|
|
37400
|
-
//#region src/workflows/
|
|
37401
|
-
|
|
37402
|
-
|
|
37403
|
-
|
|
37404
|
-
|
|
37405
|
-
|
|
37406
|
-
|
|
37407
|
-
|
|
37408
|
-
|
|
37409
|
-
|
|
37410
|
-
|
|
37411
|
-
|
|
37412
|
-
|
|
37413
|
-
|
|
37414
|
-
|
|
37415
|
-
|
|
37416
|
-
|
|
37417
|
-
|
|
37418
|
-
|
|
37249
|
+
//#region src/workflows/shared/scenarios.ts
|
|
37250
|
+
/** After a turn: stop at `scenario-wording` while wording drifted; accept moves the snapshot. */
|
|
37251
|
+
const holdWording = (frozen) => holdWording$1(frozen, (at, drifted) => human("scenario-wording", {
|
|
37252
|
+
message: scenarioWordingMessage(at, drifted),
|
|
37253
|
+
label: "Scenario wording changed",
|
|
37254
|
+
acceptClean: true,
|
|
37255
|
+
base: at
|
|
37256
|
+
}));
|
|
37257
|
+
/** `turn`, then `holdWording`; `within` names the scope the gate rests in. */
|
|
37258
|
+
const guarded = (frozen, turn, within) => async () => {
|
|
37259
|
+
await turn();
|
|
37260
|
+
if (within === void 0) await holdWording(frozen);
|
|
37261
|
+
else await scope(within, () => holdWording(frozen));
|
|
37262
|
+
};
|
|
37263
|
+
//#endregion
|
|
37264
|
+
//#region src/workflows/shared/tail.ts
|
|
37265
|
+
/** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
|
|
37266
|
+
const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
|
|
37267
|
+
/**
|
|
37268
|
+
* One review turn per lens over the whole change, each appending every
|
|
37269
|
+
* finding to `.gtd/QUALITY.md`. Resolves `"findings"` when that file
|
|
37270
|
+
* has any.
|
|
37271
|
+
*/
|
|
37272
|
+
const qualityLap = () => qualityLap$1(lensesOf(vars.qualityReviews ?? ""), reviewQuality, QUALITY);
|
|
37273
|
+
/** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
|
|
37274
|
+
const fixQualityFindings = (escalations, frozen) => fixFindings({
|
|
37275
|
+
fix: guarded(frozen, fixQuality),
|
|
37276
|
+
findings: QUALITY,
|
|
37277
|
+
cap: 3,
|
|
37278
|
+
escalate: () => escalation(escalations)
|
|
37279
|
+
});
|
|
37280
|
+
/** `.gtd/REVIEW.md` removed at the end of a round at commit `round`. */
|
|
37281
|
+
const closeReview = (round) => run("review.closing", removeScript([REVIEW, REVIEW_ORIG]), {
|
|
37282
|
+
label: "Closing the review",
|
|
37283
|
+
base: round
|
|
37284
|
+
});
|
|
37285
|
+
/** Refuses while a reviewer's notes still await `gtd door fix`. */
|
|
37286
|
+
const refusePendingHandOff = () => {
|
|
37287
|
+
if (read(".gtd/REVIEW.orig.md") !== void 0) refuse(`gtd: ${REVIEW_ORIG} is a pending hand-off — run \`gtd door fix\`, or remove ${REVIEW} and ${REVIEW_ORIG}.`);
|
|
37288
|
+
};
|
|
37289
|
+
/**
|
|
37290
|
+
* The build tail: fix (when entered red), keep green, the quality lap, then
|
|
37291
|
+
* `finish` — the human review, as the calling workflow runs it.
|
|
37292
|
+
*/
|
|
37293
|
+
const tail = (fixFirst, finish, built) => scope("build", async () => {
|
|
37294
|
+
const frozen = built?.frozen;
|
|
37295
|
+
const escalations = { rounds: 0 };
|
|
37296
|
+
const guardedFix = guarded(frozen, () => fixCheck());
|
|
37297
|
+
const repair = async () => {
|
|
37298
|
+
await guardedFix();
|
|
37299
|
+
await healthy(guardedFix, {
|
|
37300
|
+
fixesSoFar: 1,
|
|
37301
|
+
escalations
|
|
37302
|
+
});
|
|
37303
|
+
};
|
|
37304
|
+
if (fixFirst) await repair();
|
|
37305
|
+
else await healthy(guarded(frozen, () => fixCheck(built)), {
|
|
37306
|
+
escalations,
|
|
37307
|
+
sweepOnGreen: [ARCHITECTURE]
|
|
37308
|
+
});
|
|
37309
|
+
if (await qualityLap() === "findings") if (await fixQualityFindings(escalations, frozen)) await healthy(guardedFix, { escalations });
|
|
37310
|
+
else await repair();
|
|
37311
|
+
return finish(escalations, frozen);
|
|
37312
|
+
});
|
|
37313
|
+
const handOffScript = (reviewed) => [
|
|
37314
|
+
"#!/usr/bin/env sh",
|
|
37315
|
+
"set -e",
|
|
37316
|
+
`git show ${quote(`${reviewed}:${REVIEW}`)} > ${quote(REVIEW_ORIG)}`,
|
|
37317
|
+
""
|
|
37318
|
+
].join("\n");
|
|
37319
|
+
/**
|
|
37320
|
+
* The review and fix workflows' human review since `base`: a sign-off closes
|
|
37321
|
+
* `.gtd/REVIEW.md`; a note leaves it, with the reviewer's own version as
|
|
37322
|
+
* `REVIEW_ORIG`, for the next `fix` to read — never a re-plan.
|
|
37323
|
+
*/
|
|
37324
|
+
const handOffReview = (base, escalations) => reviewToHandOff(base, {
|
|
37325
|
+
file: REVIEW,
|
|
37326
|
+
reviewing,
|
|
37327
|
+
fixRisks,
|
|
37328
|
+
keepGreen: () => healthy(() => fixCheck(), { escalations }),
|
|
37329
|
+
awaitReview: (b) => awaitReview(b, handOffAwaitReviewMessage(b)),
|
|
37330
|
+
reviewMissing,
|
|
37331
|
+
close: closeReview,
|
|
37332
|
+
handOff: (reviewed) => run("review.handing-off", handOffScript(reviewed), { label: "Handing your notes to fix" }),
|
|
37333
|
+
threadsPending: "gtd land: open-threads: a thread in .gtd/REVIEW.md or an `H:` code comment waits on the agent — nothing answers it here; delete it, or turn it into a note for fix.",
|
|
37334
|
+
unparsed: "gtd land: review-doc: your edit to .gtd/REVIEW.md contains no note fix can read — restore the deleted chunk, or write the note as a line under its entry.",
|
|
37335
|
+
deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note to hand to fix."
|
|
37419
37336
|
});
|
|
37337
|
+
/** The tail the review and fix workflows run: ends at the hand-off review since `base`. */
|
|
37338
|
+
const handOffTail = (fixFirst, base) => tail(fixFirst, (escalations) => handOffReview(base, escalations));
|
|
37420
37339
|
//#endregion
|
|
37421
|
-
//#region src/workflows/
|
|
37422
|
-
|
|
37423
|
-
|
|
37424
|
-
|
|
37425
|
-
|
|
37426
|
-
|
|
37427
|
-
|
|
37428
|
-
|
|
37429
|
-
|
|
37340
|
+
//#region src/workflows/feature/steps.ts
|
|
37341
|
+
/** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
|
|
37342
|
+
const triage = (base) => agentWithSkills("triage", designTriagePrompt(base), {
|
|
37343
|
+
label: "Triaging the change",
|
|
37344
|
+
file: REQUIREMENTS,
|
|
37345
|
+
mode: "qa",
|
|
37346
|
+
model: planner(),
|
|
37347
|
+
system: designSystem()
|
|
37348
|
+
});
|
|
37349
|
+
const author = () => agentWithSkills("author", architectureAuthorPrompt(), {
|
|
37350
|
+
label: "Refining the technical plan",
|
|
37351
|
+
file: ARCHITECTURE,
|
|
37352
|
+
mode: "qa",
|
|
37353
|
+
model: planner(),
|
|
37354
|
+
system: architectSystem()
|
|
37355
|
+
});
|
|
37356
|
+
const decompose = () => agentWithSkills("decompose.decomposing", architectureDecomposePrompt(), {
|
|
37357
|
+
label: "Decomposing into packages",
|
|
37358
|
+
model: planner(),
|
|
37359
|
+
system: architectSystem()
|
|
37360
|
+
});
|
|
37361
|
+
const answerProductQuestions = () => human("gate.answer", {
|
|
37362
|
+
message: designGateAnswerMessage(),
|
|
37363
|
+
label: "Awaiting your product answers",
|
|
37364
|
+
file: REQUIREMENTS,
|
|
37365
|
+
mode: "qa",
|
|
37366
|
+
acceptClean: true
|
|
37367
|
+
});
|
|
37368
|
+
const answerTechnicalQuestions = () => human("gate.answer", {
|
|
37369
|
+
message: architectureGateAnswerMessage(),
|
|
37370
|
+
label: "Awaiting your technical answers",
|
|
37371
|
+
file: ARCHITECTURE,
|
|
37372
|
+
mode: "qa",
|
|
37373
|
+
acceptClean: true
|
|
37374
|
+
});
|
|
37375
|
+
const build = (pkg) => agentWithSkills("building", packagesItemBuildingPrompt(pkg), {
|
|
37376
|
+
label: "Building",
|
|
37377
|
+
model: coder(),
|
|
37378
|
+
system: builderSystem()
|
|
37379
|
+
});
|
|
37380
|
+
const fixSuite = () => agentWithSkills("fix.suite.fixing", packagesItemFixSuitePrompt(), {
|
|
37381
|
+
label: "Fixing the check",
|
|
37382
|
+
file: FEEDBACK,
|
|
37383
|
+
model: coder(),
|
|
37384
|
+
system: builderSystem()
|
|
37385
|
+
});
|
|
37386
|
+
/** Answer every `question` note inline in `.gtd/REVIEW.md`. */
|
|
37387
|
+
const answerReviewQuestions = (notes) => agentWithSkills("review.answer-review-questions", buildReviewAnswerQuestionsPrompt(notes), {
|
|
37388
|
+
label: "Answering your questions",
|
|
37389
|
+
file: REVIEW,
|
|
37390
|
+
mode: "review",
|
|
37391
|
+
model: planner(),
|
|
37392
|
+
system: reviewerSystem()
|
|
37393
|
+
});
|
|
37394
|
+
/** Fix every `nit` note in one turn. */
|
|
37395
|
+
const fixNits = (notes) => agentWithSkills("review.fix.nits.fixing", buildReviewFixNitsPrompt(notes), {
|
|
37396
|
+
label: "Fixing your nits",
|
|
37397
|
+
file: REVIEW,
|
|
37398
|
+
model: planner(),
|
|
37399
|
+
system: reviewerSystem()
|
|
37400
|
+
});
|
|
37401
|
+
/** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
|
|
37402
|
+
const collecting = (capture) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture), {
|
|
37403
|
+
label: "Collecting your feedback",
|
|
37404
|
+
file: REQUIREMENTS,
|
|
37405
|
+
mode: "qa",
|
|
37406
|
+
model: planner(),
|
|
37407
|
+
system: reviewerSystem(),
|
|
37408
|
+
allowEmpty: true
|
|
37409
|
+
});
|
|
37410
|
+
//#endregion
|
|
37411
|
+
//#region src/workflows/feature/packages.ts
|
|
37412
|
+
/** Package 0: the e2e scenarios, written red. */
|
|
37413
|
+
const SCENARIO_PACKAGE = ".gtd/packages/00-e2e-scenarios.md";
|
|
37414
|
+
const TEST_LINE = /^\s*-\s+(unit|e2e):\s+`([^`]+)`/;
|
|
37415
|
+
/** The `## Tests` entries of a package file; malformed lines are ignored. */
|
|
37416
|
+
const declaredTests = (packageText) => {
|
|
37417
|
+
const found = [];
|
|
37418
|
+
let inTests = false;
|
|
37419
|
+
for (const line of packageText.split("\n")) if (/^##\s/.test(line)) inTests = /^##\s+Tests\s*$/.test(line);
|
|
37420
|
+
else if (inTests) {
|
|
37421
|
+
const match = TEST_LINE.exec(line);
|
|
37422
|
+
if (match) found.push({
|
|
37423
|
+
level: match[1],
|
|
37424
|
+
path: match[2]
|
|
37425
|
+
});
|
|
37426
|
+
}
|
|
37427
|
+
return found;
|
|
37428
|
+
};
|
|
37429
|
+
/**
|
|
37430
|
+
* Refuse a build or fix turn unless every test `pkg` declares exists and is in the
|
|
37431
|
+
* diff since `since`. The package text is read as of `since`, so a builder
|
|
37432
|
+
* editing its own `## Tests` cannot dodge the guard; with `.gtd/SATISFIED.md`
|
|
37433
|
+
* written, existing in the tree suffices.
|
|
37434
|
+
*/
|
|
37435
|
+
const requireDeclaredTests = (pkg, since) => {
|
|
37436
|
+
const range = changesSince(since);
|
|
37437
|
+
const text = range.get(pkg)?.before ?? read(pkg) ?? "";
|
|
37438
|
+
const satisfied = read(".gtd/SATISFIED.md") !== void 0;
|
|
37439
|
+
const missing = declaredTests(text).map((test) => test.path).filter((path) => satisfied ? read(path) === void 0 : range.get(path) === void 0 || range.get(path)?.status === "deleted" || read(path) === void 0);
|
|
37440
|
+
if (missing.length === 0) return;
|
|
37441
|
+
refuse(`gtd land: declared-tests: ${pkg} declares tests that are ${satisfied ? "missing from the tree" : "not in the package's diff"} — write them first:\n${missing.map((path) => ` - ${path}`).join("\n")}`);
|
|
37442
|
+
};
|
|
37443
|
+
/** Build `pkg`, check its declared tests, keep the fast suite green, and close it out, which removes it. */
|
|
37444
|
+
const packageItem = async (pkg, frozen) => {
|
|
37445
|
+
const from = head$1();
|
|
37446
|
+
const declared = (turn) => async () => {
|
|
37447
|
+
await turn();
|
|
37448
|
+
requireDeclaredTests(pkg, from);
|
|
37449
|
+
};
|
|
37450
|
+
await guarded(frozen, declared(() => build(pkg)))();
|
|
37451
|
+
await healthy(guarded(frozen, declared(fixSuite)), { suite: "fast" });
|
|
37452
|
+
await run("closing", removeScript([pkg, ".gtd/SATISFIED.md"]), { label: "Closing out the package" });
|
|
37453
|
+
return {
|
|
37454
|
+
pkg,
|
|
37455
|
+
from,
|
|
37456
|
+
to: head$1()
|
|
37457
|
+
};
|
|
37458
|
+
};
|
|
37459
|
+
/** The first queued package — the one a package step works on. */
|
|
37460
|
+
const nextPackage = () => [...glob(".gtd/packages/*.md")].sort()[0];
|
|
37461
|
+
const outsideGtd = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
37462
|
+
/** Build every package file under `.gtd/packages/`, in name order. */
|
|
37463
|
+
const packages = (carried) => scope("packages", async () => {
|
|
37464
|
+
let frozen = carried;
|
|
37465
|
+
const ranges = [];
|
|
37466
|
+
let touched = [];
|
|
37467
|
+
for (let pkg = nextPackage(); pkg !== void 0; pkg = nextPackage()) {
|
|
37468
|
+
const range = await scope("item", () => packageItem(pkg, pkg === ".gtd/packages/00-e2e-scenarios.md" ? void 0 : frozen));
|
|
37469
|
+
ranges.push(range);
|
|
37470
|
+
if (pkg === ".gtd/packages/00-e2e-scenarios.md") {
|
|
37471
|
+
touched = changesSince(range.from).filter((c) => outsideGtd(c.path) && c.status !== "deleted");
|
|
37472
|
+
frozen = freezeScenarios(touched.filter((c) => c.path.endsWith(".feature")).map((c) => c.path), frozen);
|
|
37473
|
+
}
|
|
37474
|
+
}
|
|
37475
|
+
const paths = (status) => touched.filter((c) => c.status === status).map((c) => c.path);
|
|
37476
|
+
return {
|
|
37477
|
+
ranges,
|
|
37478
|
+
scenarios: {
|
|
37479
|
+
added: paths("added"),
|
|
37480
|
+
changed: paths("modified")
|
|
37481
|
+
},
|
|
37482
|
+
frozen
|
|
37483
|
+
};
|
|
37484
|
+
});
|
|
37485
|
+
//#endregion
|
|
37486
|
+
//#region src/workflows/feature/planning.ts
|
|
37487
|
+
/**
|
|
37488
|
+
* Always stop for the human. Resolves `true` when the round changed anything
|
|
37489
|
+
* (the author revises), `false` on a clean re-run. Open threads refuse every
|
|
37490
|
+
* landing; unticked questions only refuse a round with no thread for the
|
|
37491
|
+
* agent to answer.
|
|
37492
|
+
*/
|
|
37493
|
+
const noteGate = async (file, answer) => {
|
|
37494
|
+
await answer();
|
|
37495
|
+
requireThreadsClosed(file);
|
|
37496
|
+
if (changes().length === 0) return false;
|
|
37497
|
+
if (!hasThreadFor("agent", file)) requireAnswers(file);
|
|
37498
|
+
return true;
|
|
37499
|
+
};
|
|
37500
|
+
/** Triage the sketch since `base` into requirements, until no product question is open. */
|
|
37501
|
+
const design = (base) => scope("design", async () => {
|
|
37502
|
+
do {
|
|
37503
|
+
await triage(base);
|
|
37504
|
+
requireProgress(REQUIREMENTS);
|
|
37505
|
+
requireReplies(REQUIREMENTS);
|
|
37506
|
+
} while (await noteGate(REQUIREMENTS, answerProductQuestions));
|
|
37507
|
+
});
|
|
37508
|
+
/** Work out the technical plan, then decompose it into package files. */
|
|
37509
|
+
const architecture = () => scope("architecture", async () => {
|
|
37510
|
+
do {
|
|
37511
|
+
await author();
|
|
37512
|
+
requireReplies(ARCHITECTURE);
|
|
37513
|
+
} while (await noteGate(ARCHITECTURE, answerTechnicalQuestions));
|
|
37514
|
+
await decompose();
|
|
37515
|
+
if (changes(".gtd/packages/**").length === 0) refuse("gtd land: decompose: write at least one package under .gtd/packages/");
|
|
37516
|
+
if (changes(".gtd/ARCHITECTURE.md").some((c) => c.status === "deleted")) refuse("gtd land: decompose: keep .gtd/ARCHITECTURE.md — the build tail's full run needs it; it is removed only once that run is green");
|
|
37517
|
+
});
|
|
37518
|
+
//#endregion
|
|
37519
|
+
//#region src/workflows/feature/review.ts
|
|
37520
|
+
const VERDICTS = [
|
|
37521
|
+
"edit",
|
|
37522
|
+
"question",
|
|
37523
|
+
"nit",
|
|
37524
|
+
"praise"
|
|
37525
|
+
];
|
|
37526
|
+
/** One four-way choice per note the human added to `.gtd/REVIEW.md`. */
|
|
37527
|
+
const verdictQuestion = (note) => ({
|
|
37528
|
+
id: note.id,
|
|
37529
|
+
primitive: "choice",
|
|
37530
|
+
instructions: `Classify the human's note on review item "${note.anchor}" (in .gtd/REVIEW.md). Its evidence holds the anchor, the reviewer's text and the human's text.`,
|
|
37531
|
+
criteria: "edit: a request to change behaviour, design or scope — anything needing a plan, or any note you are unsure about. question: asks something and wants an answer, with no change requested. nit: a small, local, unambiguous fix (naming, typo, formatting, comment wording) needing no re-plan. praise: an approving remark with nothing to do."
|
|
37532
|
+
});
|
|
37533
|
+
/**
|
|
37534
|
+
* One judge rest, one verdict per note. Dismissing a note is the risky
|
|
37535
|
+
* direction, so only a confident non-`edit` verdict on uncut evidence counts;
|
|
37536
|
+
* a cut, unanswered or below-floor note is an `edit`, and a blank floor makes
|
|
37537
|
+
* every note one.
|
|
37538
|
+
*/
|
|
37539
|
+
const triage$1 = async (notes) => {
|
|
37540
|
+
const { answers, truncated } = await judge("review.triage", {
|
|
37541
|
+
questions: notes.map(verdictQuestion),
|
|
37542
|
+
evidence: Object.fromEntries(notes.map((n) => [n.id, `Anchor: ${n.anchor}\nReviewer's text: ${n.before}\nHuman's text: ${n.text}`])),
|
|
37543
|
+
message: buildReviewTriageMessage(),
|
|
37544
|
+
label: "Judging each note"
|
|
37545
|
+
});
|
|
37546
|
+
const minP = numeric(vars.reviewNoteActionable, Infinity);
|
|
37547
|
+
return new Map(notes.map((n) => {
|
|
37548
|
+
const verdict = VERDICTS.find((v) => v !== "edit" && answered(answers[n.id], v, minP));
|
|
37549
|
+
return [n.id, truncated.includes(n.id) || verdict === void 0 ? "edit" : verdict];
|
|
37550
|
+
}));
|
|
37551
|
+
};
|
|
37552
|
+
const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
37553
|
+
/**
|
|
37554
|
+
* A reviewer writes `.gtd/REVIEW.md` over everything since `base`, a human
|
|
37555
|
+
* reviews and signs off or comments, and each note is judged: edits go to
|
|
37556
|
+
* another planning lap, questions are answered in place, nits fixed in one
|
|
37557
|
+
* batch, praise dropped.
|
|
37558
|
+
*/
|
|
37559
|
+
const review$1 = (base, escalations = { rounds: 0 }, frozen) => review$2(base, {
|
|
37560
|
+
file: REVIEW,
|
|
37561
|
+
feedback: REQUIREMENTS,
|
|
37562
|
+
isCode,
|
|
37563
|
+
reviewing,
|
|
37564
|
+
fixRisks: (risks) => guarded(frozen, () => fixRisks(risks), "review")(),
|
|
37565
|
+
keepGreen: () => healthy(guarded(frozen, fixCheck, "review"), { escalations }),
|
|
37566
|
+
awaitReview,
|
|
37567
|
+
reviewMissing,
|
|
37568
|
+
collecting,
|
|
37569
|
+
triage: triage$1,
|
|
37570
|
+
answerQuestions: answerReviewQuestions,
|
|
37571
|
+
fixNits: (notes) => guarded(frozen, () => fixNits(notes), "review")(),
|
|
37572
|
+
close: closeReview,
|
|
37573
|
+
captures: {
|
|
37574
|
+
edits: reviewEditsCapture,
|
|
37575
|
+
notes: reviewNotesCapture,
|
|
37576
|
+
editNotes: reviewEditNotesCapture
|
|
37577
|
+
},
|
|
37578
|
+
refusals: {
|
|
37579
|
+
deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note (or edit code) to request changes.",
|
|
37580
|
+
unmatched: "gtd land: no declared pattern matches the pending changes — write .gtd/REQUIREMENTS.md from the feedback, or change nothing when it asks for nothing"
|
|
37581
|
+
}
|
|
37430
37582
|
});
|
|
37583
|
+
/** The build tail: fix (when entered red), keep green, the quality lap, then human review since `base`. */
|
|
37584
|
+
const buildTail = (fixFirst, base, built) => tail(fixFirst, (escalations, frozen) => review$1(base, escalations, frozen), built);
|
|
37431
37585
|
//#endregion
|
|
37432
|
-
//#region src/workflows/
|
|
37586
|
+
//#region src/workflows/feature/feature.ts
|
|
37433
37587
|
/** Revert the sketch that started the process out of the working tree; its intent survives in history. */
|
|
37434
37588
|
const unwind = () => {
|
|
37435
37589
|
const commit = head$1();
|
|
@@ -37479,6 +37633,7 @@ const ordinaryStart = async () => {
|
|
|
37479
37633
|
label: "Idle",
|
|
37480
37634
|
file: ".gtd/TODO.md"
|
|
37481
37635
|
});
|
|
37636
|
+
refusePendingHandOff();
|
|
37482
37637
|
await unwind();
|
|
37483
37638
|
if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
|
|
37484
37639
|
message: unwindFailedMessage(),
|
|
@@ -37488,36 +37643,108 @@ const ordinaryStart = async () => {
|
|
|
37488
37643
|
await gate("start-gate", startGateBlockedMessage());
|
|
37489
37644
|
await planAndBuild(start());
|
|
37490
37645
|
};
|
|
37491
|
-
|
|
37492
|
-
|
|
37493
|
-
|
|
37494
|
-
|
|
37495
|
-
|
|
37496
|
-
|
|
37646
|
+
const feature = shipped(ordinaryStart);
|
|
37647
|
+
//#endregion
|
|
37648
|
+
//#region src/workflows/shared/skills.ts
|
|
37649
|
+
const skills = (vars) => ({
|
|
37650
|
+
design: ["spec-driven-development", "planning-and-task-breakdown"],
|
|
37651
|
+
architecture: [
|
|
37652
|
+
"api-and-interface-design",
|
|
37653
|
+
"documentation-and-adrs",
|
|
37654
|
+
"ponytail"
|
|
37655
|
+
],
|
|
37656
|
+
"architecture.decompose": ["incremental-implementation", "planning-and-task-breakdown"],
|
|
37657
|
+
"packages.item": ["test-driven-development", "incremental-implementation"],
|
|
37658
|
+
"packages.item.fix.suite": ["debugging-and-error-recovery"],
|
|
37659
|
+
"packages.item.health": ["debugging-and-error-recovery"],
|
|
37660
|
+
build: ["debugging-and-error-recovery"],
|
|
37661
|
+
"build.health": ["debugging-and-error-recovery"],
|
|
37662
|
+
"build.fix.quality": ["incremental-implementation", "code-simplification"],
|
|
37663
|
+
"build.review": ["code-review-and-quality"],
|
|
37664
|
+
"build.review.fix.nits": ["incremental-implementation", "code-simplification"],
|
|
37665
|
+
"build.review.fix.risks": ["debugging-and-error-recovery", "incremental-implementation"],
|
|
37666
|
+
notes: ["incremental-implementation", "code-simplification"],
|
|
37667
|
+
...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), (lens) => builtInLenses[lens]?.skills ?? [lens])
|
|
37668
|
+
});
|
|
37669
|
+
//#endregion
|
|
37670
|
+
//#region src/workflows/shared/access.ts
|
|
37671
|
+
const access = (vars) => ({
|
|
37672
|
+
design: { write: [] },
|
|
37673
|
+
architecture: { write: [".gtd/REQUIREMENTS.md"] },
|
|
37674
|
+
"architecture.decompose": { write: [".gtd/packages/**", ".gtd/ARCHITECTURE.md"] },
|
|
37675
|
+
"build.review": { write: [".gtd/REVIEW.md"] },
|
|
37676
|
+
"build.review.fix.nits": {},
|
|
37677
|
+
"build.review.fix.risks": {},
|
|
37678
|
+
...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), () => ({ write: [] }))
|
|
37679
|
+
});
|
|
37680
|
+
//#endregion
|
|
37681
|
+
//#region src/workflows/review/review.ts
|
|
37682
|
+
/**
|
|
37683
|
+
* Pure review of everything since `reviewBase`: a green gate, the quality
|
|
37684
|
+
* lap, then the human review. A note ends the process with `.gtd/REVIEW.md`
|
|
37685
|
+
* left for `fix`; it never re-plans.
|
|
37686
|
+
*/
|
|
37497
37687
|
const reviewFlow = async () => {
|
|
37688
|
+
refusePendingHandOff();
|
|
37498
37689
|
await gate("review-gate", reviewGateBlockedMessage());
|
|
37499
|
-
|
|
37690
|
+
await handOffTail(false, start());
|
|
37500
37691
|
};
|
|
37501
|
-
const
|
|
37502
|
-
|
|
37503
|
-
|
|
37692
|
+
const review = shipped(reviewFlow, { review: {
|
|
37693
|
+
args: [{
|
|
37694
|
+
name: "base",
|
|
37695
|
+
optional: true
|
|
37696
|
+
}],
|
|
37697
|
+
vars: ({ base }) => base === void 0 ? {} : { reviewBase: base }
|
|
37698
|
+
} }, (_workflow, vars) => vars.reviewBase || "");
|
|
37699
|
+
/** @deprecated The `review` factory carries its own base; kept so a hand-written `review` re-exporting it still maps `reviewBase`. */
|
|
37700
|
+
const base = (workflow, vars) => workflow === "review" ? vars.reviewBase ?? "" : void 0;
|
|
37701
|
+
//#endregion
|
|
37702
|
+
//#region src/workflows/fix/fix.ts
|
|
37703
|
+
/**
|
|
37704
|
+
* Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
|
|
37705
|
+
* risk its reviewer marked, in one turn; the review files go once it is done.
|
|
37706
|
+
*/
|
|
37707
|
+
const fixReviewNotes = () => scope("notes", async () => {
|
|
37708
|
+
const record = read(".gtd/REVIEW.md") ?? "";
|
|
37709
|
+
const orig = read(REVIEW_ORIG);
|
|
37710
|
+
if (orig === void 0) return refuse(`gtd: ${REVIEW} has no ${REVIEW_ORIG}, so it was not handed off by a review — remove it, or restore ${REVIEW_ORIG}.`);
|
|
37711
|
+
const notes = reviewNotes(orig, record);
|
|
37712
|
+
await agentWithSkills("fixing", fixReviewNotesPrompt(notes, reviewRisks(record)), {
|
|
37713
|
+
label: "Fixing your review notes",
|
|
37714
|
+
file: REVIEW,
|
|
37715
|
+
model: coder(),
|
|
37716
|
+
system: finisherSystem(),
|
|
37717
|
+
allowEmpty: true
|
|
37718
|
+
});
|
|
37719
|
+
await run("closing", removeScript([REVIEW, REVIEW_ORIG]), { label: "Closing the review" });
|
|
37720
|
+
});
|
|
37721
|
+
/**
|
|
37722
|
+
* Fix what a review handed on, else repair a red baseline — then the
|
|
37723
|
+
* hand-off review of the result. A note there is handed to the next `fix`.
|
|
37724
|
+
*/
|
|
37725
|
+
const fixFlow = async () => {
|
|
37726
|
+
if (read(".gtd/REVIEW.md") !== void 0) {
|
|
37727
|
+
await fixReviewNotes();
|
|
37728
|
+
return handOffTail(false, start());
|
|
37729
|
+
}
|
|
37730
|
+
if (read(".gtd/REVIEW.orig.md") !== void 0) return refuse(`gtd: ${REVIEW_ORIG} has no ${REVIEW} — remove it, or restore ${REVIEW}.`);
|
|
37731
|
+
if (await baseline("fix-precheck")) return;
|
|
37732
|
+
await handOffTail(true, start());
|
|
37733
|
+
};
|
|
37734
|
+
const fix = shipped(fixFlow, { fix: {} });
|
|
37735
|
+
//#endregion
|
|
37736
|
+
//#region src/workflows/index.ts
|
|
37737
|
+
var workflows_default = feature;
|
|
37504
37738
|
const summary = summaryPrompt;
|
|
37505
37739
|
const steering = {
|
|
37506
37740
|
[REQUIREMENTS]: "qa",
|
|
37507
37741
|
[ARCHITECTURE]: "qa",
|
|
37508
37742
|
[REVIEW]: "review"
|
|
37509
37743
|
};
|
|
37510
|
-
|
|
37511
|
-
|
|
37512
|
-
|
|
37513
|
-
|
|
37514
|
-
|
|
37515
|
-
args: [{
|
|
37516
|
-
name: "base",
|
|
37517
|
-
optional: true
|
|
37518
|
-
}],
|
|
37519
|
-
vars: ({ base }) => base === void 0 ? {} : { reviewBase: base }
|
|
37520
|
-
}
|
|
37521
|
-
};
|
|
37744
|
+
/**
|
|
37745
|
+
* @deprecated Empty: each shipped workflow builds its own doors, so a config
|
|
37746
|
+
* gets `fix` and `review [base]` by exporting `fix` and `review`.
|
|
37747
|
+
*/
|
|
37748
|
+
const doors = {};
|
|
37522
37749
|
//#endregion
|
|
37523
|
-
export { ARCHITECTURE, ESCALATION, FEEDBACK, FIX_CAP, QUALITY, REQUIREMENTS, REVIEW, SCENARIO_PACKAGE, SETUP, ShippedOptions, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, awaitReview, base, baseline, build, buildTail, builtInLenses, collecting, declaredTests, decompose,
|
|
37750
|
+
export { ARCHITECTURE, ESCALATION, FEEDBACK, FIX_CAP, QUALITY, REQUIREMENTS, REVIEW, REVIEW_ORIG, SCENARIO_PACKAGE, SETUP, ShippedOptions, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, awaitReview, base, baseline, build, buildTail, builtInLenses, closeReview, coder, collecting, declaredTests, decompose, workflows_default as default, defaults, describeEscalation, design, doors, driftedScenarios, envDefaults, escalate, escalation, escalationExhausted, fastSuiteGuard, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, fixSuite, freezeScenarios, gate, guarded, handOffReview, handOffTail, healthy, holdWording, lensesOf, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, planner, qualityLap, reUnwind, requireDeclaredTests, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewing, scenarioText, skills, steering, summary, tail, threads, triage, unwind, wordingDrift };
|