@pmelab/gtd 30.0.0 → 31.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 +23 -9
- package/claude/hooks/register.tsx +5 -0
- package/dist/chunks/{reserved-iG86IR3I.mjs → reserved-CHTJTzpA.mjs} +68 -9
- package/dist/chunks/{tail-DA2UuJNP.mjs → tail-CCsDBbZe.mjs} +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +141 -11
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +207 -18
- package/package.json +1 -1
- package/src/workflows/feature/acceptance-criteria-tier.feature +25 -0
- package/src/workflows/feature/acceptance-criteria.feature +189 -0
- package/src/workflows/feature/criteria.test.ts +106 -0
- package/src/workflows/feature/criteria.ts +168 -0
- package/src/workflows/feature/fast-path.feature +4 -0
- package/src/workflows/feature/feature.test.ts +81 -10
- package/src/workflows/feature/feature.ts +18 -10
- package/src/workflows/feature/flag-mode.feature +165 -0
- package/src/workflows/feature/index.ts +1 -0
- package/src/workflows/feature/one-step-land.feature +4 -0
- package/src/workflows/feature/planning.ts +2 -0
- package/src/workflows/feature/refactor-absorbs.feature +4 -0
- package/src/workflows/feature/refactor-first.feature +4 -0
- package/src/workflows/feature/refactor-review.feature +24 -0
- package/src/workflows/feature/risk-tier.feature +32 -0
- package/src/workflows/feature/seam-rejection.feature +8 -0
- package/src/workflows/feature/seam.feature +8 -0
- package/src/workflows/feature/seam.ts +14 -7
- package/src/workflows/feature/tier-pin.feature +4 -0
- package/src/workflows/index.ts +1 -0
- package/src/workflows/shared/options.test.ts +12 -0
- package/src/workflows/shared/options.ts +17 -2
- package/src/workflows/shared/text.test.ts +16 -0
- package/src/workflows/shared/text.ts +90 -8
- package/src/workflows/shared/vars.ts +1 -0
package/README.md
CHANGED
|
@@ -418,18 +418,24 @@ noted in steps 3 and 4 below.
|
|
|
418
418
|
with `- A: <reply>` and the process stops at the same gate again. Close a
|
|
419
419
|
thread by replying with a conclusion or deleting it. While a thread's last
|
|
420
420
|
entry is the agent's, moving on is refused. Leave the file untouched and
|
|
421
|
-
start the loop to accept the plan as-is, unanswered questions and all.
|
|
421
|
+
start the loop to accept the plan as-is, unanswered questions and all. The
|
|
422
|
+
file lists numbered acceptance criteria (`AC-1`, `AC-2`, …) that bind the
|
|
423
|
+
build. Sketch your own under an `Acceptance criteria` heading and, when a
|
|
424
|
+
judge finds every one clear and checkable, they are recorded as they are and
|
|
425
|
+
this moment is skipped; one vague criterion brings it back. See
|
|
426
|
+
[Acceptance criteria](docs/configuration.md#acceptance-criteria).
|
|
422
427
|
|
|
423
428
|
3. **You wait.** The agent first implements the requirements the quickest way
|
|
424
429
|
that works — the naive pass — and a judge reads where it changed code (see
|
|
425
|
-
[the naive pass](#the-naive-pass) below
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
430
|
+
[the naive pass](#the-naive-pass) below; with [flag mode](#the-naive-pass)
|
|
431
|
+
off, neither runs). Then the change is built test-first, checked against the
|
|
432
|
+
full suite (`testCommand`) and fixed until it passes — so keep that suite
|
|
433
|
+
fast; gtd has no lighter one. Scenario wording is not frozen: you sign the
|
|
434
|
+
scenarios off once, in the review's `## Scenarios` chunk, where any assertion
|
|
435
|
+
an existing scenario lost is marked `Weakened:`. After the build the quality
|
|
436
|
+
lap runs. Three points along the process (two with flag mode off) are judged
|
|
437
|
+
rather than always asking you outright — each stops and hands you a verdict
|
|
438
|
+
to make (`gtd judge answer`, or land with a clean tree to accept the
|
|
433
439
|
conservative default, which never skips work;
|
|
434
440
|
`gtd judge run --provider fixed --answers <path>` — or the
|
|
435
441
|
`GTD_JUDGE_ANSWERS` env var, inline JSON — answers one from a file, piped
|
|
@@ -595,6 +601,14 @@ out is `gtd abandon`. A process from 27.5.0 or later whose judge ruled
|
|
|
595
601
|
that slice. Details in
|
|
596
602
|
[the naive-pass path](docs/configuration.md#the-naive-pass-path).
|
|
597
603
|
|
|
604
|
+
**Flag mode (`flagMode`, on by default).** On, the implementation is asked for a
|
|
605
|
+
new release flag (none at a `low` risk tier) and the naive pass and its judge
|
|
606
|
+
run as above. A project that is not publicly released turns it off explicitly —
|
|
607
|
+
`feature({ flagMode: false })` in `gtd.config.ts`, it is never detected: then no
|
|
608
|
+
flag is asked for, no seam judge or refactor runs, and the implementation is the
|
|
609
|
+
naive version, straight through the health loop and the quality lap. See
|
|
610
|
+
[`flagMode`](docs/configuration.md#the-bundled-workflows-settings).
|
|
611
|
+
|
|
598
612
|
**Risk tier (flag `GTD_FLAG_RISK_TIER=1`, off by default).** Triage proposes a
|
|
599
613
|
risk tier, `low` or `high`, and the production signals to watch after merge.
|
|
600
614
|
Edit the first word under `## Risk Tier` at the design gate to change it; a
|
|
@@ -993,6 +993,11 @@ export const register: Register = (on) => {
|
|
|
993
993
|
return next(e)
|
|
994
994
|
})
|
|
995
995
|
|
|
996
|
+
// A worker's tool call hands settings hooks the MAIN session's
|
|
997
|
+
// `transcript_path`, marked only by `agent_id`/`agent_type`; its own
|
|
998
|
+
// transcript (`agent_transcript_path`) is named only at SubagentStop, too
|
|
999
|
+
// late to measure a running worker's context.
|
|
1000
|
+
//
|
|
996
1001
|
// The loop lands a turn when it ends; work left running in the background
|
|
997
1002
|
// would still be writing the tree gtd is committing.
|
|
998
1003
|
on("tool.call", async ($, e, next) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { accessFor, agent, answered, changesSince, check, codeThreads, defineWorkflow, displayPath, env, head, human, judge, numeric, quote, read, refuse, scope, skillsFor, start, vars } from "../flows.mjs";
|
|
2
2
|
import { escalation as escalation$1, gate as gate$1, healthy as healthy$1, lensScopes, lensesOf, removedAssertions } from "../workflow-kit.mjs";
|
|
3
|
-
import { c as String$, g as standardSchemaV1, h as optionalWith, l as Struct } from "./Schema-DO5ttJaI.mjs";
|
|
3
|
+
import { c as String$, g as standardSchemaV1, h as optionalWith, l as Struct, n as Boolean$ } from "./Schema-DO5ttJaI.mjs";
|
|
4
4
|
//#region src/workflows/shared/registry.ts
|
|
5
5
|
const FILL = "";
|
|
6
6
|
const commentLength = (rest) => {
|
|
@@ -132,6 +132,7 @@ const defaults = {
|
|
|
132
132
|
reviewSeverityMinP: "0.7",
|
|
133
133
|
refactorFlagPattern: "\\bGTD_FLAG_\\w+",
|
|
134
134
|
seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
|
|
135
|
+
flagMode: "true",
|
|
135
136
|
flag_risk_tier: "0",
|
|
136
137
|
qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge",
|
|
137
138
|
refactorQualityReviews: "correctness, ponytail-review, conventions"
|
|
@@ -438,6 +439,21 @@ const codeThreadReplies = () => {
|
|
|
438
439
|
concluded thread in and delete its lines:
|
|
439
440
|
${waiting.map((t) => ` - ${t.path}:${t.line}: ${t.first}`).join("\n")}`;
|
|
440
441
|
};
|
|
442
|
+
const acceptanceCriteriaBlock = `
|
|
443
|
+
## Acceptance criteria
|
|
444
|
+
|
|
445
|
+
Whichever lap this is, \`.gtd/REQUIREMENTS.md\` carries a
|
|
446
|
+
\`## Acceptance Criteria\` section: one \`- AC-<n>: <criterion>\` line
|
|
447
|
+
per criterion, numbered from \`AC-1\`. They bind every later step —
|
|
448
|
+
the build is done when each one holds — so each must be checkable: a
|
|
449
|
+
test or a command shows it holds or not. A later lap keeps every ID:
|
|
450
|
+
reword a criterion in place, append a new one with the next unused
|
|
451
|
+
number, delete a dropped one — never renumber, never reuse an ID. When
|
|
452
|
+
the file holds no list (a review loop-back rewrites it), recover the
|
|
453
|
+
last settled one from history (\`git log -p -- .gtd/REQUIREMENTS.md\`)
|
|
454
|
+
and carry its IDs over. Landing is refused while a criterion has no ID or
|
|
455
|
+
two share one.
|
|
456
|
+
`;
|
|
441
457
|
const riskTierBlock = `
|
|
442
458
|
## Risk tier and production signals
|
|
443
459
|
|
|
@@ -481,7 +497,7 @@ ${footnoteFoldIn}${codeThreadReplies()}
|
|
|
481
497
|
or both
|
|
482
498
|
- Whichever lap this is, leave \`.gtd/REQUIREMENTS.md\`
|
|
483
499
|
uncommitted and finish once it reflects this lap's own work
|
|
484
|
-
|
|
500
|
+
${acceptanceCriteriaBlock}
|
|
485
501
|
## First lap
|
|
486
502
|
|
|
487
503
|
\`.gtd/REQUIREMENTS.md\` does not exist yet (or holds whatever the
|
|
@@ -536,6 +552,34 @@ ${questionBarReturn}
|
|
|
536
552
|
made). If red, make that breakage the first concern, ahead of
|
|
537
553
|
everything else — the build assumes the suite started green
|
|
538
554
|
${riskTier ? riskTierBlock : ""}`;
|
|
555
|
+
const clearCriteriaQuestion = {
|
|
556
|
+
id: "clear",
|
|
557
|
+
primitive: "noul",
|
|
558
|
+
instructions: "state.criteria lists the acceptance criteria the human's sketch (state.sketch) stated, as gtd numbered them. Decide whether they can stand as the binding definition of done, with no design conversation.",
|
|
559
|
+
criteria: "yes: every criterion is clear and checkable — a test or a command can show it holds or not — and together they say what to build. no: any one criterion is vague, subjective or untestable, or the sketch leaves an open product question. Unsure means no."
|
|
560
|
+
};
|
|
561
|
+
const clearCriteriaMessage = () => `The sketch stated its own acceptance criteria. Run \`gtd judge answer\` and
|
|
562
|
+
pipe \`yes\` when every one is clear and checkable, so design is skipped and
|
|
563
|
+
they are recorded as they are — or land without a verdict to run the
|
|
564
|
+
ordinary design conversation.
|
|
565
|
+
`;
|
|
566
|
+
const criterionLine = (c) => `- ${c.id}: ${c.text.replace(/\s+/g, " ")}`;
|
|
567
|
+
/** Writes \`.gtd/REQUIREMENTS.md\` from criteria the sketch stated, wrapped the way oxfmt writes it. */
|
|
568
|
+
const normaliseCriteriaScript = (stated) => {
|
|
569
|
+
return `mkdir -p .gtd
|
|
570
|
+
printf '%s\\n' ${[
|
|
571
|
+
"# Requirements",
|
|
572
|
+
"",
|
|
573
|
+
`Stated in \`${stated.path}\` by the sketch:`,
|
|
574
|
+
"",
|
|
575
|
+
`\`git show ${stated.commit}:${stated.path}\``,
|
|
576
|
+
"",
|
|
577
|
+
"## Acceptance Criteria",
|
|
578
|
+
"",
|
|
579
|
+
...stated.criteria.flatMap((c) => wrapItem(criterionLine(c)))
|
|
580
|
+
].map(quote).join(" ")} > ${quote(".gtd/REQUIREMENTS.md")}
|
|
581
|
+
`;
|
|
582
|
+
};
|
|
539
583
|
const designSystem = () => `${designPersona}
|
|
540
584
|
|
|
541
585
|
${agentConduct}`;
|
|
@@ -860,7 +904,8 @@ What each change does next (then run \`gtd land\`):
|
|
|
860
904
|
const wrapItem = (item) => {
|
|
861
905
|
const out = [];
|
|
862
906
|
let line = "";
|
|
863
|
-
|
|
907
|
+
const words = item.split(" ").reduce((acc, word) => acc.length > 0 && /^([-+*>#]|\d+[.)])$/.test(word) ? [...acc.slice(0, -1), `${acc.at(-1)} ${word}`] : [...acc, word], []);
|
|
908
|
+
for (const word of words) if (line !== "" && line.length + 1 + word.length > 80) {
|
|
864
909
|
out.push(line);
|
|
865
910
|
line = ` ${word}`;
|
|
866
911
|
} else line = line === "" ? word : `${line} ${word}`;
|
|
@@ -1158,8 +1203,12 @@ Print the closing message and stop — this writes nothing itself.
|
|
|
1158
1203
|
`;
|
|
1159
1204
|
};
|
|
1160
1205
|
const listed = (paths, unreadable = []) => paths.map((path) => ` - \`${displayPath(path)}\`${unreadable.includes(path) ? " (unreadable)" : ""}`).join("\n");
|
|
1206
|
+
const criteriaBinding = (refused) => `- Its \`## Acceptance Criteria\` (\`AC-<n>\`) bind this turn: re-read them from
|
|
1207
|
+
the file, never from memory, and stop only when each one holds. They are
|
|
1208
|
+
settled — ${refused ? "a turn that changes them is refused" : "leave them as they are"}`;
|
|
1161
1209
|
const naivePassPrompt = (rejection) => `${stateFileRules}
|
|
1162
1210
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
1211
|
+
${criteriaBinding(false)}
|
|
1163
1212
|
- Implement them the quickest way that works — the naive version. Do not
|
|
1164
1213
|
restructure, abstract or refactor around the change; a later step judges
|
|
1165
1214
|
the shape of your diff and a reviewer makes it fit
|
|
@@ -1172,18 +1221,21 @@ re-implement. It changed these files outside the expected set:
|
|
|
1172
1221
|
${listed(rejection.strays)}
|
|
1173
1222
|
Start over and choose a different approach; do not rebuild the same diff.
|
|
1174
1223
|
`}`;
|
|
1175
|
-
const implementBuildingPrompt = (reference, tier, amend, afterRefactor) => `${stateFileRules}
|
|
1224
|
+
const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true) => `${stateFileRules}
|
|
1176
1225
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
1226
|
+
${criteriaBinding(true)}
|
|
1177
1227
|
${afterRefactor === true ? `- A reviewed refactor has just landed and may already carry part of the change,
|
|
1178
1228
|
though never a release flag. Build only what is still missing, held to every
|
|
1179
1229
|
rule below; if nothing is, change nothing and finish your turn
|
|
1180
1230
|
` : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
|
|
1181
1231
|
the re-derived requirements instead of rewriting it
|
|
1182
|
-
` : `- Implement them properly, fitting the codebase's seams and conventions
|
|
1232
|
+
` : flagMode ? `- Implement them properly, fitting the codebase's seams and conventions
|
|
1233
|
+
` : `- Implement them the quickest way that works — the naive version. Do not
|
|
1234
|
+
restructure, abstract or refactor around the change
|
|
1183
1235
|
`}${reference === void 0 || amend === true ? "" : `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
|
|
1184
1236
|
Read it for ideas; never apply, cherry-pick or check it out — write
|
|
1185
1237
|
the change fresh
|
|
1186
|
-
`}${tier === "low" ? "- Add no release flag — this change is low risk tier" : amend === true ? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag" : `- Put the new behaviour behind a new release flag, following the
|
|
1238
|
+
`}${!flagMode ? "- Add no release flag — this project ships without release flags" : tier === "low" ? "- Add no release flag — this change is low risk tier" : amend === true ? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag" : `- Put the new behaviour behind a new release flag, following the
|
|
1187
1239
|
repository's flag conventions`}
|
|
1188
1240
|
- Write the tests first, then the code that makes them pass
|
|
1189
1241
|
- Leave \`.gtd/REQUIREMENTS.md\` in place and everything uncommitted, then
|
|
@@ -1420,12 +1472,19 @@ const ShippedOptions = Struct({
|
|
|
1420
1472
|
refactorQualityReviews: setting,
|
|
1421
1473
|
testCommand: setting,
|
|
1422
1474
|
plannerModel: setting,
|
|
1423
|
-
coderModel: setting
|
|
1475
|
+
coderModel: setting,
|
|
1476
|
+
flagMode: optionalWith(Boolean$, { exact: true })
|
|
1424
1477
|
});
|
|
1425
1478
|
const over = (base, options) => ({
|
|
1426
1479
|
...base,
|
|
1427
|
-
...Object.fromEntries(Object.entries(options).filter(([name]) => Object.hasOwn(base, name)))
|
|
1480
|
+
...Object.fromEntries(Object.entries(options).filter(([name]) => Object.hasOwn(base, name)).map(([name, value]) => [name, String(value)]))
|
|
1428
1481
|
});
|
|
1482
|
+
/** The `flagMode` setting, pinned at process start. Anything but true/false fails rather than guess a side. */
|
|
1483
|
+
const flagModeOn = () => {
|
|
1484
|
+
const value = vars.flagMode ?? defaults.flagMode;
|
|
1485
|
+
if (value !== "true" && value !== "false") refuse(`gtd: flagMode must be "true" or "false", got ${JSON.stringify(value)}`);
|
|
1486
|
+
return value === "true";
|
|
1487
|
+
};
|
|
1429
1488
|
/** A shipped flow as a factory: its options override the settings it declares, each in its own kind. */
|
|
1430
1489
|
const shipped = (flow, doors, base) => defineWorkflow(standardSchemaV1(ShippedOptions, {
|
|
1431
1490
|
onExcessProperty: "error",
|
|
@@ -1509,4 +1568,4 @@ const steering = {
|
|
|
1509
1568
|
[REVIEW]: "review"
|
|
1510
1569
|
};
|
|
1511
1570
|
//#endregion
|
|
1512
|
-
export {
|
|
1571
|
+
export { reviewEditNotesCapture as $, buildReviewCollectingPrompt as A, designTriagePrompt as B, planner as C, agentWithSkills as D, reviewing as E, clearCriteriaQuestion as F, implementBuildingPrompt as G, fixReviewNotesPrompt as H, criterionLine as I, normaliseCriteriaScript as J, implementFixPrompt as K, deferNotesScript as L, buildReviewTriageMessage as M, builderSystem as N, buildReviewAnswerQuestionsPrompt as O, clearCriteriaMessage as P, refactorRules as Q, designGateAnswerMessage as R, fixRisks as S, reviewQuality as T, handOffAwaitReviewMessage as U, finisherSystem as V, idleMessage as W, refactorDroppedRefusal as X, refactorBuildingPrompt as Y, refactorFixPrompt as Z, REVIEW as _, flagModeOn as a, seamSteerMessage as at, fixCheck as b, baseline as c, unwindFailure as ct, guarded as d, testFileGlobs as dt, reviewEditsCapture as et, healthy as f, flagMentions as ft, REQUIREMENTS as g, requireFlag as gt, REFACTOR as h, registryEntry as ht, skills as i, seamByHandMessage as it, buildReviewFixNitsPrompt as j, buildReviewAwaitAfterDeferMessage as k, escalation as l, defaults as lt, QUALITY as m, flipProblems as mt, summary as n, reviewNotesCapture as nt, flagShipped as o, startGateBlockedMessage as ot, FEEDBACK as p, flagReferences as pt, naivePassPrompt as q, access as r, reviewerSystem as rt, shipped as s, unwindFailedMessage as st, steering as t, reviewGateBlockedMessage as tt, gate as u, envDefaults as ut, awaitReview as v, reviewMissing as w, fixQuality as x, coder as y, designSystem as z };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, showScript, vars } from "../flows.mjs";
|
|
2
2
|
import { fixFindings, lensesOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { E as reviewing, S as fixRisks, T as reviewQuality, U as handOffAwaitReviewMessage, _ as REVIEW, b as fixCheck, d as guarded, f as healthy, l as escalation, m as QUALITY, v as awaitReview, w as reviewMissing, x as fixQuality } from "./reserved-CHTJTzpA.mjs";
|
|
4
4
|
//#region src/workflows/shared/tail.ts
|
|
5
5
|
/** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
|
|
6
6
|
const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { gt as requireFlag, ht as registryEntry, i as skills, lt as defaults, n as summary, o as flagShipped, pt as flagReferences, r as access, t as steering, ut as envDefaults, y as coder } from "../chunks/reserved-CHTJTzpA.mjs";
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-CCsDBbZe.mjs";
|
|
4
4
|
//#region src/workflows/cleanup/cleanup.ts
|
|
5
5
|
const removingPrompt = (name, registry) => `Clean up the release flag "${name}", which is default-on: delete its entry in ${registry}, and remove every \`GTD_FLAG_${name.toUpperCase()}\` check from the shipped sources. Keep the flag-on path; delete code only the off path reached, including tests that pin the off path.`;
|
|
6
6
|
const cleanupFlow = async () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { answered, changes, changesSince, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, slice, start, vars } from "../flows.mjs";
|
|
2
2
|
import { lensesOf, review as review$1 } from "../workflow-kit.mjs";
|
|
3
|
-
import { $ as
|
|
4
|
-
import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-
|
|
3
|
+
import { $ as reviewEditNotesCapture, A as buildReviewCollectingPrompt, B as designTriagePrompt, C as planner, D as agentWithSkills, E as reviewing, F as clearCriteriaQuestion, G as implementBuildingPrompt, I as criterionLine, J as normaliseCriteriaScript, K as implementFixPrompt, L as deferNotesScript, M as buildReviewTriageMessage, N as builderSystem, O as buildReviewAnswerQuestionsPrompt, P as clearCriteriaMessage, Q as refactorRules, R as designGateAnswerMessage, S as fixRisks, W as idleMessage, X as refactorDroppedRefusal, Y as refactorBuildingPrompt, Z as refactorFixPrompt, _ as REVIEW, a as flagModeOn, at as seamSteerMessage, b as fixCheck, ct as unwindFailure, d as guarded, dt as testFileGlobs, et as reviewEditsCapture, f as healthy, g as REQUIREMENTS, h as REFACTOR, i as skills, it as seamByHandMessage, j as buildReviewFixNitsPrompt, k as buildReviewAwaitAfterDeferMessage, lt as defaults, n as summary, nt as reviewNotesCapture, ot as startGateBlockedMessage, p as FEEDBACK, q as naivePassPrompt, r as access, rt as reviewerSystem, s as shipped, st as unwindFailedMessage, t as steering, u as gate, ut as envDefaults, v as awaitReview, w as reviewMissing, y as coder, z as designSystem } from "../chunks/reserved-CHTJTzpA.mjs";
|
|
4
|
+
import { a as refusePendingHandOff, i as isOptionalObject, n as closeReview, o as tail } from "../chunks/tail-CCsDBbZe.mjs";
|
|
5
5
|
//#region src/workflows/shared/fences.ts
|
|
6
6
|
const markFenced = (text) => {
|
|
7
7
|
let fence;
|
|
@@ -17,6 +17,128 @@ const markFenced = (text) => {
|
|
|
17
17
|
});
|
|
18
18
|
};
|
|
19
19
|
//#endregion
|
|
20
|
+
//#region src/workflows/feature/criteria.ts
|
|
21
|
+
/** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
|
|
22
|
+
const CLEAR_MIN_P = .8;
|
|
23
|
+
const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
24
|
+
const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
|
|
25
|
+
const ID = /^(AC-\d+):\s*(.*)$/;
|
|
26
|
+
/** The list items under the first heading `title` matches (at `level` when given), up to the next heading; wrapped lines joined, fenced lines ignored. */
|
|
27
|
+
const listUnder = (text, title, level) => {
|
|
28
|
+
const lines = markFenced(text);
|
|
29
|
+
const at = lines.findIndex((l) => {
|
|
30
|
+
const heading = l.fenced ? null : HEADING.exec(l.line);
|
|
31
|
+
return heading !== null && (level === void 0 || heading[1].length === level) && title.test(heading[2]);
|
|
32
|
+
});
|
|
33
|
+
if (at === -1) return void 0;
|
|
34
|
+
const items = [];
|
|
35
|
+
let open = false;
|
|
36
|
+
for (const { line, fenced } of lines.slice(at + 1)) {
|
|
37
|
+
if (fenced) {
|
|
38
|
+
open = false;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (HEADING.test(line)) break;
|
|
42
|
+
const item = ITEM.exec(line);
|
|
43
|
+
if (item !== null) {
|
|
44
|
+
items.push(item[1].trim());
|
|
45
|
+
open = true;
|
|
46
|
+
} else if (open && line.trim() !== "") items[items.length - 1] = `${items.at(-1)} ${line.trim()}`.trim();
|
|
47
|
+
else open = false;
|
|
48
|
+
}
|
|
49
|
+
return items;
|
|
50
|
+
};
|
|
51
|
+
/** The `- AC-<n>: …` list of `## Acceptance Criteria`, or `undefined` when the section is missing. */
|
|
52
|
+
const criteriaOf = (text) => listUnder(text, /^acceptance criteria$/i, 2)?.map((item) => {
|
|
53
|
+
const id = ID.exec(item);
|
|
54
|
+
return id === null ? {
|
|
55
|
+
id: "",
|
|
56
|
+
text: item
|
|
57
|
+
} : {
|
|
58
|
+
id: id[1],
|
|
59
|
+
text: id[2]
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
/** Items numbered `AC-1`…, unless every one already carries its own distinct ID. */
|
|
63
|
+
const numbered = (items) => {
|
|
64
|
+
const ids = items.map((item) => ID.exec(item));
|
|
65
|
+
const own = ids.every((id) => id !== null) && new Set(ids.map((id) => id[1])).size === ids.length;
|
|
66
|
+
return items.map((item, i) => own ? {
|
|
67
|
+
id: ids[i][1],
|
|
68
|
+
text: ids[i][2]
|
|
69
|
+
} : {
|
|
70
|
+
id: `AC-${i + 1}`,
|
|
71
|
+
text: item.replace(ID, "$2")
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
const STATED = /^acceptance criteria:?$/i;
|
|
75
|
+
const statedItems = (text) => text === void 0 ? [] : (listUnder(text, STATED) ?? []).filter((item) => item !== "");
|
|
76
|
+
/**
|
|
77
|
+
* The criteria a `TODO.md` in the sketch `sketch` (the idle turn's changes)
|
|
78
|
+
* added or changed under an `Acceptance criteria` heading. More than one such
|
|
79
|
+
* file is ambiguous, and any other change in the sketch is input only triage
|
|
80
|
+
* folds in, so none.
|
|
81
|
+
*/
|
|
82
|
+
const statedCriteria = (sketch, commit) => {
|
|
83
|
+
const found = sketch.flatMap((change) => {
|
|
84
|
+
if (!/(^|\/)TODO\.md$/.test(change.path) || change.after === void 0) return [];
|
|
85
|
+
const items = statedItems(change.after);
|
|
86
|
+
if (items.length === 0 || items.join("\n") === statedItems(change.before).join("\n")) return [];
|
|
87
|
+
return [{
|
|
88
|
+
path: change.path,
|
|
89
|
+
commit,
|
|
90
|
+
text: change.after,
|
|
91
|
+
criteria: numbered(items)
|
|
92
|
+
}];
|
|
93
|
+
});
|
|
94
|
+
return found.length === 1 && sketch.length === 1 ? found[0] : void 0;
|
|
95
|
+
};
|
|
96
|
+
const listing = (criteria) => criteria.map(criterionLine).join("\n");
|
|
97
|
+
/** Refuse a turn that leaves `file` without ID'd acceptance criteria. A deleted file is `requireProgress`'s call. */
|
|
98
|
+
const requireCriteria = (file) => {
|
|
99
|
+
const text = read(file);
|
|
100
|
+
if (text === void 0) return;
|
|
101
|
+
const criteria = criteriaOf(text) ?? [];
|
|
102
|
+
const unnamed = criteria.filter((c) => c.id === "");
|
|
103
|
+
const ids = criteria.map((c) => c.id).filter((id) => id !== "");
|
|
104
|
+
const twice = ids.filter((id, i) => ids.indexOf(id) !== i);
|
|
105
|
+
if (criteria.length > 0 && unnamed.length === 0 && twice.length === 0) return;
|
|
106
|
+
refuse(`gtd land: acceptance-criteria: ${file} has ${criteria.length === 0 ? `no "## Acceptance Criteria" list` : unnamed.length > 0 ? `${unnamed.length} criterion without an ID:\n${unnamed.map((c) => ` - ${c.text}`).join("\n")}` : `duplicate IDs: ${[...new Set(twice)].join(", ")}`} — list every criterion under "## Acceptance Criteria" as "- AC-<n>: <checkable criterion>", each ID unique and never reused`);
|
|
107
|
+
};
|
|
108
|
+
/** Refuse a turn that changed the acceptance criteria in `file`: after the design gate only another design lap may. */
|
|
109
|
+
const requireCriteriaFrozen = (file) => {
|
|
110
|
+
const change = changes().get(file);
|
|
111
|
+
if (change === void 0) return;
|
|
112
|
+
if (listing(criteriaOf(change.before ?? "") ?? []) === listing(criteriaOf(change.after ?? "") ?? [])) return;
|
|
113
|
+
refuse(`gtd land: criteria-frozen: the acceptance criteria in ${file} are settled — restore them; only a design lap changes them`);
|
|
114
|
+
};
|
|
115
|
+
/** Whether the judge trusts `stated` as clear and checkable, every criterion of it. Truncated evidence never is. */
|
|
116
|
+
const clear = async (stated) => {
|
|
117
|
+
const { answers, truncated } = await judge("clear", {
|
|
118
|
+
questions: [clearCriteriaQuestion],
|
|
119
|
+
evidence: {
|
|
120
|
+
criteria: listing(stated.criteria),
|
|
121
|
+
sketch: stated.text
|
|
122
|
+
},
|
|
123
|
+
message: clearCriteriaMessage(),
|
|
124
|
+
label: "Judging the stated acceptance criteria"
|
|
125
|
+
});
|
|
126
|
+
return truncated.length === 0 && answered(answers.clear, "yes", CLEAR_MIN_P);
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* The skip path: when the sketch stated clear, checkable criteria, write them
|
|
130
|
+
* to `.gtd/REQUIREMENTS.md` as they are and resolve `true` — no triage, no
|
|
131
|
+
* design gate. `false` when design must run.
|
|
132
|
+
*/
|
|
133
|
+
const skipDesign = (stated) => stated === void 0 ? Promise.resolve(false) : scope("design", async () => {
|
|
134
|
+
if (!await clear(stated)) return false;
|
|
135
|
+
await run("normalise", normaliseCriteriaScript(stated), {
|
|
136
|
+
label: "Recording the stated acceptance criteria",
|
|
137
|
+
file: REQUIREMENTS
|
|
138
|
+
});
|
|
139
|
+
return true;
|
|
140
|
+
});
|
|
141
|
+
//#endregion
|
|
20
142
|
//#region src/workflows/feature/tier.ts
|
|
21
143
|
/** GTD_FLAG_RISK_TIER: pinned at process start, so a mid-process flip never moves a process. */
|
|
22
144
|
const riskTierOn = () => vars.flag_risk_tier === "1";
|
|
@@ -137,6 +259,7 @@ const design = (base) => scope("design", async () => {
|
|
|
137
259
|
await triage(base);
|
|
138
260
|
requireProgress(REQUIREMENTS);
|
|
139
261
|
requireReplies(REQUIREMENTS);
|
|
262
|
+
requireCriteria(REQUIREMENTS);
|
|
140
263
|
} while (await noteGate(REQUIREMENTS, answerProductQuestions));
|
|
141
264
|
});
|
|
142
265
|
//#endregion
|
|
@@ -625,18 +748,22 @@ function implement(first, tier, amend) {
|
|
|
625
748
|
amend
|
|
626
749
|
};
|
|
627
750
|
return scope("implement", async () => {
|
|
628
|
-
await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor), {
|
|
751
|
+
await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn()), {
|
|
629
752
|
label: "Implementing",
|
|
630
753
|
model: coder(),
|
|
631
754
|
system: builderSystem(),
|
|
632
755
|
...options.afterRefactor === true ? { allowEmpty: true } : {}
|
|
633
756
|
});
|
|
757
|
+
requireCriteriaFrozen(REQUIREMENTS);
|
|
634
758
|
await healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
|
|
635
759
|
label: "Fixing the check",
|
|
636
760
|
file: FEEDBACK,
|
|
637
761
|
model: coder(),
|
|
638
762
|
system: builderSystem()
|
|
639
|
-
})
|
|
763
|
+
}), {
|
|
764
|
+
guard: () => requireCriteriaFrozen(REQUIREMENTS),
|
|
765
|
+
guardRests: false
|
|
766
|
+
});
|
|
640
767
|
await run("closing", removeScript([REQUIREMENTS]), { label: "Closing out the implementation" });
|
|
641
768
|
});
|
|
642
769
|
}
|
|
@@ -667,6 +794,7 @@ const decide = () => scope("seam", async () => {
|
|
|
667
794
|
acceptClean: true
|
|
668
795
|
});
|
|
669
796
|
if (changes().length === 0) return { reference: tip.hash };
|
|
797
|
+
requireCriteria(REQUIREMENTS);
|
|
670
798
|
rejection = void 0;
|
|
671
799
|
retries = 0;
|
|
672
800
|
}
|
|
@@ -706,10 +834,10 @@ const unwind = () => {
|
|
|
706
834
|
const commit = head();
|
|
707
835
|
return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
|
|
708
836
|
};
|
|
709
|
-
/** A judged naive pass, then implement and review until a review round signs off; design must have run since `firstBase`. */
|
|
837
|
+
/** A judged naive pass (none with `flagMode` off), then implement and review until a review round signs off; design must have run since `firstBase`. */
|
|
710
838
|
const buildFeature = async (firstBase) => {
|
|
711
839
|
const tier = riskTierOn() ? recordRisk() : void 0;
|
|
712
|
-
const seam = await seamBuild(firstBase);
|
|
840
|
+
const seam = flagModeOn() ? await seamBuild(firstBase) : { base: firstBase };
|
|
713
841
|
await slice("feature", async () => {
|
|
714
842
|
await implement({
|
|
715
843
|
reference: seam.reference,
|
|
@@ -723,9 +851,9 @@ const buildFeature = async (firstBase) => {
|
|
|
723
851
|
}), base);
|
|
724
852
|
});
|
|
725
853
|
};
|
|
726
|
-
/** Design
|
|
727
|
-
const planAndBuild = async (firstBase) => {
|
|
728
|
-
await design(firstBase);
|
|
854
|
+
/** Design — skipped when the sketch's own criteria are judged clear — then `buildFeature`. */
|
|
855
|
+
const planAndBuild = async (firstBase, stated) => {
|
|
856
|
+
if (!await skipDesign(riskTierOn() ? void 0 : stated)) await design(firstBase);
|
|
729
857
|
await buildFeature(firstBase);
|
|
730
858
|
};
|
|
731
859
|
const featureFeedback = async (first, lensBase) => {
|
|
@@ -754,6 +882,8 @@ const ordinaryStart = async () => {
|
|
|
754
882
|
file: ".gtd/TODO.md"
|
|
755
883
|
});
|
|
756
884
|
refusePendingHandOff();
|
|
885
|
+
flagModeOn();
|
|
886
|
+
const stated = statedCriteria(changes(), head());
|
|
757
887
|
await unwind();
|
|
758
888
|
if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
|
|
759
889
|
message: unwindFailedMessage(),
|
|
@@ -761,11 +891,11 @@ const ordinaryStart = async () => {
|
|
|
761
891
|
file: FEEDBACK
|
|
762
892
|
});
|
|
763
893
|
await gate("start-gate", startGateBlockedMessage());
|
|
764
|
-
await planAndBuild(start());
|
|
894
|
+
await planAndBuild(start(), stated);
|
|
765
895
|
};
|
|
766
896
|
const feature = shipped(ordinaryStart);
|
|
767
897
|
//#endregion
|
|
768
898
|
//#region src/workflows/feature/index.ts
|
|
769
899
|
var feature_default = feature;
|
|
770
900
|
//#endregion
|
|
771
|
-
export { REFACTOR_PACKAGE, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, collecting, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, implement, judgeSeam, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, steering, summary, triage, unwind };
|
|
901
|
+
export { REFACTOR_PACKAGE, access, afterTail, answerProductQuestions, answerReviewQuestions, buildFeature, buildTail, collecting, criteriaOf, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, implement, judgeSeam, naivePass, ordinaryStart, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireCriteria, requireCriteriaFrozen, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, skipDesign, statedCriteria, steering, summary, triage, unwind };
|
package/dist/workflows/fix.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
|
|
2
2
|
import { d as reviewNotes, f as reviewRisks } from "../chunks/steering-DVILanVf.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-
|
|
3
|
+
import { D as agentWithSkills, H as fixReviewNotesPrompt, V as finisherSystem, _ as REVIEW, c as baseline, i as skills, lt as defaults, n as summary, r as access, s as shipped, t as steering, ut as envDefaults, y as coder } from "../chunks/reserved-CHTJTzpA.mjs";
|
|
4
|
+
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-CCsDBbZe.mjs";
|
|
5
5
|
//#region src/workflows/fix/fix.ts
|
|
6
6
|
/**
|
|
7
7
|
* Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { start } from "../flows.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { i as skills, lt as defaults, n as summary, r as access, s as shipped, t as steering, tt as reviewGateBlockedMessage, u as gate, ut as envDefaults } from "../chunks/reserved-CHTJTzpA.mjs";
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-CCsDBbZe.mjs";
|
|
4
4
|
//#region src/workflows/review/review.ts
|
|
5
5
|
/**
|
|
6
6
|
* Pure review of everything since `reviewBase`: a green gate, the quality
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { ft as flagMentions, gt as requireFlag, i as skills, lt as defaults, mt as flipProblems, n as summary, o as flagShipped, p as FEEDBACK, r as access, t as steering, u as gate, ut as envDefaults, y as coder } from "../chunks/reserved-CHTJTzpA.mjs";
|
|
3
3
|
//#region src/workflows/rollout/rollout.ts
|
|
4
4
|
const flippingPrompt = (name, registry) => `Roll out the release flag "${name}": in ${registry}, set \`defaultOn: true\` on its entry. Change nothing else — no other entry, no other field, no other file, no comments.`;
|
|
5
5
|
const suiteBlockedMessage = () => "The suite is red after the rollout. Fix it by hand — a rollout may only touch the flag's registry entry — then land.";
|