@pmelab/gtd 27.1.0 → 27.3.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 +16 -9
- package/dist/chunks/registry-CP3YuV9j.mjs +101 -0
- package/dist/chunks/{reserved-BL9krPao.mjs → reserved-BPdVZkgv.mjs} +66 -73
- package/dist/chunks/tail-iLAMoigl.mjs +81 -0
- package/dist/gtd.bundle.mjs +5 -2
- package/dist/testkit.mjs +17 -2
- package/dist/workflows/cleanup.mjs +37 -0
- package/dist/workflows/feature.mjs +119 -5
- package/dist/workflows/fix.mjs +2 -1
- package/dist/workflows/review.mjs +2 -1
- package/dist/workflows/rollout.mjs +33 -0
- package/dist/workflows.mjs +362 -25
- package/package.json +11 -1
- package/src/flags/index.test.ts +28 -0
- package/src/flags/index.ts +9 -4
- package/src/testkit/EmittedScriptRecognizer.ts +21 -0
- package/src/testkit/EmittedScripts.test.ts +9 -0
- package/src/testkit/steps.ts +26 -0
- package/src/testkit/testkit.feature +8 -1
- package/src/workflows/cleanup/cleanup.feature +323 -0
- package/src/workflows/cleanup/cleanup.test.ts +272 -0
- package/src/workflows/cleanup/cleanup.ts +52 -0
- package/src/workflows/cleanup/index.ts +6 -0
- package/src/workflows/feature/index.ts +1 -0
- package/src/workflows/feature/refactor.test.ts +466 -0
- package/src/workflows/feature/refactor.ts +178 -0
- package/src/workflows/index.ts +4 -1
- package/src/workflows/rollout/index.ts +6 -0
- package/src/workflows/rollout/rollout.feature +214 -0
- package/src/workflows/rollout/rollout.test.ts +167 -0
- package/src/workflows/rollout/rollout.ts +44 -0
- package/src/workflows/shared/access.ts +2 -1
- package/src/workflows/shared/health.test.ts +103 -0
- package/src/workflows/shared/health.ts +32 -7
- package/src/workflows/shared/options.test.ts +30 -0
- package/src/workflows/shared/options.ts +20 -0
- package/src/workflows/shared/registry.test.ts +305 -0
- package/src/workflows/shared/registry.ts +158 -0
- package/src/workflows/shared/skills.test.ts +6 -0
- package/src/workflows/shared/skills.ts +5 -1
- package/src/workflows/shared/tail.test.ts +36 -2
- package/src/workflows/shared/tail.ts +30 -18
- package/src/workflows/shared/text.ts +35 -0
- package/src/workflows/shared/vars.ts +1 -0
package/README.md
CHANGED
|
@@ -552,6 +552,13 @@ unknown key. `gtd.config.ts` is the only repository config: gtd 23 removed the
|
|
|
552
552
|
home directory — fails every command until you delete it. See
|
|
553
553
|
[Migrating from `.gtdrc`](https://github.com/pmelab/gtd/blob/main/docs/configuration.md#migrating-from-gtdrc).
|
|
554
554
|
|
|
555
|
+
### Rolling out and cleaning up release flags
|
|
556
|
+
|
|
557
|
+
Two more bundled workflows retire a release flag:
|
|
558
|
+
`gtd --workflow rollout --var flag=<name>` turns it default-on,
|
|
559
|
+
`gtd --workflow cleanup --var flag=<name>` removes it afterwards. See
|
|
560
|
+
[Feature flags](docs/configuration.md#rolling-out-and-cleaning-up-a-release-flag).
|
|
561
|
+
|
|
555
562
|
## Checking a range for stray files
|
|
556
563
|
|
|
557
564
|
```sh
|
|
@@ -599,15 +606,15 @@ are not process history: `gtd history` does not list them, and the
|
|
|
599
606
|
gtd is one package, `@pmelab/gtd`, and every entrypoint shares its version and
|
|
600
607
|
[SemVer promise](https://github.com/pmelab/gtd/blob/main/COMPATIBILITY.md).
|
|
601
608
|
|
|
602
|
-
| Import
|
|
603
|
-
|
|
|
604
|
-
| `@pmelab/gtd/flows`
|
|
605
|
-
| `@pmelab/gtd/wire`
|
|
606
|
-
| `@pmelab/gtd/steering`
|
|
607
|
-
| `@pmelab/gtd/workflow-kit`
|
|
608
|
-
| `@pmelab/gtd/workflows`
|
|
609
|
-
| `@pmelab/gtd/workflows/feature`, `@pmelab/gtd/workflows/review`, `@pmelab/gtd/workflows/fix` | One shipped workflow each: its factory as the default export, its parts, and the reserved exports (`defaults`, `envDefaults`, `summary`, `steering`, `skills`, `access`). `gtd install` imports these. |
|
|
610
|
-
| `@pmelab/gtd/testkit`
|
|
609
|
+
| Import | What it is |
|
|
610
|
+
| -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
611
|
+
| `@pmelab/gtd/flows` | The flow API a workflow is written in: the steps (`agent`, `human`, `run`, …), workflow factories, `defineConfig` and `FLOWS_PROTOCOL`. |
|
|
612
|
+
| `@pmelab/gtd/wire` | The driver protocol: `WIRE_SCHEMA`, the exit codes and the types of every `--json` document. |
|
|
613
|
+
| `@pmelab/gtd/steering` | The steering-file formats (`qa`, `review`) and the review-note readers. |
|
|
614
|
+
| `@pmelab/gtd/workflow-kit` | Policy-free building blocks to compose a workflow from: health checks, quality laps, scenario freezing, review tails. |
|
|
615
|
+
| `@pmelab/gtd/workflows` | Every shipped workflow and all its parts in one module; also resolvable as `@pmelab/gtd/workflow`. |
|
|
616
|
+
| `@pmelab/gtd/workflows/feature`, `@pmelab/gtd/workflows/review`, `@pmelab/gtd/workflows/fix`, `@pmelab/gtd/workflows/rollout`, `@pmelab/gtd/workflows/cleanup` | One shipped workflow each: its factory as the default export, its parts, and the reserved exports (`defaults`, `envDefaults`, `summary`, `steering`, `skills`, `access`). `gtd install` imports these. |
|
|
617
|
+
| `@pmelab/gtd/testkit` | Runs any workflow against the real engine, in an in-memory or a real repository. Needs `vitest` and `quickpickle`, optional peer dependencies. |
|
|
611
618
|
|
|
612
619
|
Types come from the TypeScript sources the package ships. Configs never need an
|
|
613
620
|
install for any of these but `./testkit`: gtd resolves a `gtd.config.ts`'s
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { read, refuse } from "../flows.mjs";
|
|
2
|
+
//#region src/workflows/shared/registry.ts
|
|
3
|
+
const FILL = "";
|
|
4
|
+
const commentLength = (rest) => {
|
|
5
|
+
const end = rest.startsWith("//") ? rest.indexOf("\n") : rest.indexOf("*/") + 2;
|
|
6
|
+
return end <= 0 ? rest.length : end;
|
|
7
|
+
};
|
|
8
|
+
const stringEnd = (text, i, quote) => {
|
|
9
|
+
let j = i + 1;
|
|
10
|
+
while (j < text.length && text[j] !== quote) j += text[j] === "\\" ? 2 : 1;
|
|
11
|
+
return Math.min(j, text.length);
|
|
12
|
+
};
|
|
13
|
+
const mask = (text) => {
|
|
14
|
+
let out = "";
|
|
15
|
+
let i = 0;
|
|
16
|
+
while (i < text.length) {
|
|
17
|
+
const c = text[i];
|
|
18
|
+
const rest = text.slice(i);
|
|
19
|
+
if (rest.startsWith("//") || rest.startsWith("/*")) {
|
|
20
|
+
const len = commentLength(rest);
|
|
21
|
+
out += FILL.repeat(len);
|
|
22
|
+
i += len;
|
|
23
|
+
} else if (c === "\"" || c === "'" || c === "`") {
|
|
24
|
+
const j = stringEnd(text, i, c);
|
|
25
|
+
out += c + FILL.repeat(j - i - 1) + (j < text.length ? c : "");
|
|
26
|
+
i = j + 1;
|
|
27
|
+
} else {
|
|
28
|
+
out += c;
|
|
29
|
+
i += 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
};
|
|
34
|
+
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
35
|
+
const entrySpan = (text, name) => {
|
|
36
|
+
const quoted = escapeRegExp(name);
|
|
37
|
+
const re = new RegExp(`\\bname\\s*:\\s*(["'\`])${quoted}\\1`, "g");
|
|
38
|
+
const masked = mask(text);
|
|
39
|
+
for (const hit of text.matchAll(re)) {
|
|
40
|
+
if (masked[hit.index] === FILL) continue;
|
|
41
|
+
const start = masked.lastIndexOf("{", hit.index);
|
|
42
|
+
const close = masked.indexOf("}", hit.index);
|
|
43
|
+
return start < 0 || close < 0 ? void 0 : {
|
|
44
|
+
start,
|
|
45
|
+
end: close + 1
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const DEFAULT_ON = /\bdefaultOn\s*:\s*(true|false)\s*,?/;
|
|
50
|
+
const defaultOnHit = (body) => DEFAULT_ON.exec(mask(body));
|
|
51
|
+
const defaultOnValue = (body) => defaultOnHit(body)?.[1];
|
|
52
|
+
const KIND = /\bkind\s*:\s*["'`]/;
|
|
53
|
+
const registryEntry = (text, name) => {
|
|
54
|
+
const span = entrySpan(text, name);
|
|
55
|
+
if (span === void 0) return void 0;
|
|
56
|
+
const body = text.slice(span.start, span.end);
|
|
57
|
+
const hit = KIND.exec(mask(body));
|
|
58
|
+
return {
|
|
59
|
+
kind: (hit === null ? null : /^(\w+)["'`]/.exec(body.slice(hit.index + hit[0].length)))?.[1] ?? "",
|
|
60
|
+
defaultOn: defaultOnValue(body) === "true"
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
const canonical = (body) => {
|
|
64
|
+
const hit = defaultOnHit(body);
|
|
65
|
+
const kept = hit === null ? body : body.slice(0, hit.index) + body.slice(hit.index + hit[0].length);
|
|
66
|
+
const masked = mask(kept);
|
|
67
|
+
return kept.split("").filter((c, i) => masked[i] === FILL || !(/\s/.test(c) || c === "," && /^\s*}/.test(masked.slice(i + 1)))).join("");
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* [] when `after` differs from `before` only by the named entry's `defaultOn`
|
|
71
|
+
* going absent/false → true (whitespace and trailing commas inside that entry
|
|
72
|
+
* ignored, so oxfmt re-wrapping it passes); else one line per fault.
|
|
73
|
+
*/
|
|
74
|
+
const flipProblems = (before, after, name) => {
|
|
75
|
+
const was = entrySpan(before, name);
|
|
76
|
+
const now = entrySpan(after, name);
|
|
77
|
+
if (was === void 0) return [`no entry "${name}" before the turn`];
|
|
78
|
+
if (now === void 0) return [`entry "${name}" is gone`];
|
|
79
|
+
const faults = [];
|
|
80
|
+
if (before.slice(0, was.start) !== after.slice(0, now.start)) faults.push("text before the entry changed");
|
|
81
|
+
if (before.slice(was.end) !== after.slice(now.end)) faults.push("text after the entry changed");
|
|
82
|
+
const oldBody = before.slice(was.start, was.end);
|
|
83
|
+
const newBody = after.slice(now.start, now.end);
|
|
84
|
+
if (canonical(oldBody) !== canonical(newBody)) faults.push(`entry "${name}" changed beyond defaultOn`);
|
|
85
|
+
if (defaultOnValue(newBody) !== "true") faults.push(`"${name}" is not set to defaultOn: true`);
|
|
86
|
+
return faults;
|
|
87
|
+
};
|
|
88
|
+
/** `path:line` for every `GTD_FLAG_<NAME>` in shipped sources; re-states scripts/lint-flags.ts' rule by hand. */
|
|
89
|
+
const flagReferences = (files, name) => {
|
|
90
|
+
const pattern = new RegExp(`\\bGTD_FLAG_${escapeRegExp(name.toUpperCase())}\\b`);
|
|
91
|
+
return files.filter(({ path }) => !/\.(test|fixture|stories)\.tsx?$/.test(path)).flatMap(({ path, text }) => text.split("\n").flatMap((line, i) => pattern.test(line) ? [`${path}:${i + 1}`] : []));
|
|
92
|
+
};
|
|
93
|
+
const requireFlag = (workflow, registryPath, name, defaultOn) => {
|
|
94
|
+
if (name === "") return refuse(`gtd: ${workflow} needs --var flag=<name>`);
|
|
95
|
+
const entry = registryEntry(read(registryPath) ?? "", name);
|
|
96
|
+
if (entry === void 0) return refuse(`gtd: ${workflow}: no flag "${name}" in ${registryPath}`);
|
|
97
|
+
if (entry.kind !== "release") return refuse(`gtd: ${workflow}: flag "${name}" is ${entry.kind}, not release`);
|
|
98
|
+
if (entry.defaultOn !== defaultOn) return refuse(defaultOn ? `gtd: ${workflow}: flag "${name}" is not default-on — roll it out first` : `gtd: ${workflow}: flag "${name}" is already default-on`);
|
|
99
|
+
};
|
|
100
|
+
//#endregion
|
|
101
|
+
export { requireFlag as i, flipProblems as n, registryEntry as r, flagReferences as t };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { accessFor, agent, answered, changesSince, check, codeThreads, defineWorkflow, env, head, human, judge, numeric, quote,
|
|
2
|
-
import { escalation as escalation$1,
|
|
1
|
+
import { accessFor, agent, answered, changesSince, check, codeThreads, defineWorkflow, env, head, human, judge, numeric, quote, skillsFor, start, vars } from "../flows.mjs";
|
|
2
|
+
import { escalation as escalation$1, gate as gate$1, healthy as healthy$1, lensScopes, lensesOf, removedAssertions } from "../workflow-kit.mjs";
|
|
3
3
|
import { c as String$, g as standardSchemaV1, h as optionalWith, l as Struct } from "./Schema-CyImoHTj.mjs";
|
|
4
4
|
//#region src/workflows/shared/prose.ts
|
|
5
5
|
const skillsPreamble = `- Load whatever's listed here that your harness actually has installed,
|
|
@@ -591,6 +591,26 @@ ${fixFeedbackPrompt}
|
|
|
591
591
|
then legitimately report itself already satisfied later
|
|
592
592
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
593
593
|
`;
|
|
594
|
+
const refactorRules = (strays, pattern) => `- Behaviour-neutral: the suite's outcome must not change
|
|
595
|
+
- Change only these paths, nothing else — a test file only when it is
|
|
596
|
+
one of them:
|
|
597
|
+
${strays.map((path) => ` - \`${path}\``).join("\n")}
|
|
598
|
+
- Introduce no new flag read and add no second read of an existing one:
|
|
599
|
+
every match of \`${pattern}\` may move, never increase in number`;
|
|
600
|
+
const refactorBuildingPrompt = (pkg, strays, pattern, reference) => `${stateFileRules}
|
|
601
|
+
- The package to implement is \`${pkg}\`; never delete it
|
|
602
|
+
${refactorRules(strays, pattern)}
|
|
603
|
+
${reference === void 0 ? "" : `- Reference only: \`git show ${reference} -- ${strays.map(quote).join(" ")}\` shows an
|
|
604
|
+
earlier attempt at this change. Read it for ideas; never apply,
|
|
605
|
+
cherry-pick or check it out — write a fresh refactor
|
|
606
|
+
`}- Leave everything uncommitted and finish your turn — do not commit
|
|
607
|
+
`;
|
|
608
|
+
const refactorFixPrompt = (strays, pattern) => `${stateFileRules}
|
|
609
|
+
${fixFeedbackPrompt}
|
|
610
|
+
${refactorRules(strays, pattern)}
|
|
611
|
+
- Never do another package's work, even when it would green the suite
|
|
612
|
+
- Leave everything uncommitted and finish your turn — do not commit
|
|
613
|
+
`;
|
|
594
614
|
const healthJudgeMessage = () => `The check is still red, and this isn't the first round —
|
|
595
615
|
\`.gtd/FEEDBACK.md\` holds this round's output, and the previous round's
|
|
596
616
|
is in history. Run \`gtd judge answer\` and pipe a
|
|
@@ -1157,12 +1177,22 @@ const escalationSteps = {
|
|
|
1157
1177
|
stop: escalate,
|
|
1158
1178
|
exhausted: escalationExhausted
|
|
1159
1179
|
};
|
|
1180
|
+
const guarded = (turn, guard) => async (...a) => {
|
|
1181
|
+
await turn(...a);
|
|
1182
|
+
guard?.();
|
|
1183
|
+
};
|
|
1184
|
+
const guardedSteps = (guard, describeGuard) => ({
|
|
1185
|
+
limit: escalationSteps.limit,
|
|
1186
|
+
describe: guarded(guarded(escalationSteps.describe, guard), describeGuard),
|
|
1187
|
+
stop: guarded(escalationSteps.stop, guard),
|
|
1188
|
+
exhausted: guarded(escalationSteps.exhausted, guard)
|
|
1189
|
+
});
|
|
1160
1190
|
/**
|
|
1161
1191
|
* Hand a red suite that repeated attempts could not fix to a person: an
|
|
1162
1192
|
* agent writes `.gtd/ESCALATION.md`, a human narrows it. From the third round
|
|
1163
1193
|
* on, the human is told the budget is spent instead.
|
|
1164
1194
|
*/
|
|
1165
|
-
const escalation = (count) => escalation$1(count,
|
|
1195
|
+
const escalation = (count, guard) => escalation$1(count, guardedSteps(guard));
|
|
1166
1196
|
/** "Is this red round the same failure as the last one?" — asked of two reports, or of nothing when one is empty. */
|
|
1167
1197
|
const retryQuestion = (comparable) => comparable ? {
|
|
1168
1198
|
id: "verdict",
|
|
@@ -1198,21 +1228,25 @@ const runSuite = (options) => check("health.check", env.testCommand ?? "", {
|
|
|
1198
1228
|
* first once `FIX_CAP` fixes are spent, or a judge calls the failure
|
|
1199
1229
|
* identical to the previous round's.
|
|
1200
1230
|
*/
|
|
1201
|
-
const healthy = (fix, options = {}) => healthy$1(fix, {
|
|
1231
|
+
const healthy = (fix, options = {}) => healthy$1(guarded(fix, options.guard), {
|
|
1202
1232
|
check: () => runSuite(options),
|
|
1203
1233
|
report: FEEDBACK,
|
|
1204
1234
|
fixCap: 3,
|
|
1205
1235
|
fixesSoFar: options.fixesSoFar ?? 0,
|
|
1206
1236
|
sameFailure,
|
|
1207
1237
|
escalations: options.escalations ?? { rounds: 0 },
|
|
1208
|
-
escalation:
|
|
1238
|
+
escalation: guardedSteps(options.guard, options.describeGuard)
|
|
1209
1239
|
});
|
|
1210
|
-
/**
|
|
1211
|
-
|
|
1240
|
+
/**
|
|
1241
|
+
* Hold the process at `<name>.blocked` until the suite is green. `guard` runs
|
|
1242
|
+
* after each `blocked` landing only — never after a check, whose refusal would
|
|
1243
|
+
* re-run the same script forever.
|
|
1244
|
+
*/
|
|
1245
|
+
const gate = (name, message, guard) => gate$1(name, () => baseline("check"), guarded(() => human("blocked", {
|
|
1212
1246
|
message,
|
|
1213
1247
|
label: "Baseline is red",
|
|
1214
1248
|
file: FEEDBACK
|
|
1215
|
-
}));
|
|
1249
|
+
}), guard));
|
|
1216
1250
|
//#endregion
|
|
1217
1251
|
//#region src/workflows/shared/vars.ts
|
|
1218
1252
|
/** Process settings. */
|
|
@@ -1222,6 +1256,7 @@ const defaults = {
|
|
|
1222
1256
|
judgeIdenticalMinP: "0.7",
|
|
1223
1257
|
reviewNoteActionable: "0.7",
|
|
1224
1258
|
reviewSeverityMinP: "0.7",
|
|
1259
|
+
refactorFlagPattern: "\\bGTD_FLAG_\\w+",
|
|
1225
1260
|
qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge"
|
|
1226
1261
|
};
|
|
1227
1262
|
/** Environment settings. */
|
|
@@ -1239,6 +1274,7 @@ const ShippedOptions = Struct({
|
|
|
1239
1274
|
judgeIdenticalMinP: setting,
|
|
1240
1275
|
reviewNoteActionable: setting,
|
|
1241
1276
|
reviewSeverityMinP: setting,
|
|
1277
|
+
refactorFlagPattern: setting,
|
|
1242
1278
|
qualityReviews: setting,
|
|
1243
1279
|
testCommand: setting,
|
|
1244
1280
|
plannerModel: setting,
|
|
@@ -1259,73 +1295,27 @@ const shipped = (flow, doors, base) => defineWorkflow(standardSchemaV1(ShippedOp
|
|
|
1259
1295
|
doors,
|
|
1260
1296
|
base
|
|
1261
1297
|
}));
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
|
|
1266
|
-
/**
|
|
1267
|
-
* One review turn per lens over the whole change, each appending every
|
|
1268
|
-
* finding to `.gtd/QUALITY.md`. Resolves `"findings"` when that file
|
|
1269
|
-
* has any.
|
|
1270
|
-
*/
|
|
1271
|
-
const qualityLap = () => qualityLap$1(lensesOf(vars.qualityReviews ?? ""), reviewQuality, QUALITY);
|
|
1272
|
-
/** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
|
|
1273
|
-
const fixQualityFindings = (escalations) => fixFindings({
|
|
1274
|
-
fix: fixQuality,
|
|
1275
|
-
findings: QUALITY,
|
|
1276
|
-
cap: 3,
|
|
1277
|
-
escalate: () => escalation(escalations)
|
|
1298
|
+
const FlagOptions = Struct({
|
|
1299
|
+
...ShippedOptions.fields,
|
|
1300
|
+
flagRegistry: setting
|
|
1278
1301
|
});
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
};
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
const escalations = { rounds: 0 };
|
|
1295
|
-
const fix = fixCheck;
|
|
1296
|
-
const repair = async () => {
|
|
1297
|
-
await fix();
|
|
1298
|
-
await healthy(fix, {
|
|
1299
|
-
fixesSoFar: 1,
|
|
1300
|
-
escalations
|
|
1301
|
-
});
|
|
1302
|
+
const flagShipped = (flow) => defineWorkflow(standardSchemaV1(FlagOptions, {
|
|
1303
|
+
onExcessProperty: "error",
|
|
1304
|
+
errors: "all"
|
|
1305
|
+
}), (options) => {
|
|
1306
|
+
const flagDefaults = {
|
|
1307
|
+
...defaults,
|
|
1308
|
+
flag: "",
|
|
1309
|
+
flagRegistry: "src/flags/index.ts"
|
|
1310
|
+
};
|
|
1311
|
+
return {
|
|
1312
|
+
flow,
|
|
1313
|
+
defaults: over(flagDefaults, options),
|
|
1314
|
+
envDefaults: over(envDefaults, options),
|
|
1315
|
+
doors: {},
|
|
1316
|
+
base: void 0
|
|
1302
1317
|
};
|
|
1303
|
-
if (fixFirst) await repair();
|
|
1304
|
-
else if (!green) await healthy(fix, { escalations });
|
|
1305
|
-
if (await qualityLap() === "findings") if (await fixQualityFindings(escalations)) await healthy(fix, { escalations });
|
|
1306
|
-
else await repair();
|
|
1307
|
-
return finish(escalations);
|
|
1308
|
-
});
|
|
1309
|
-
/**
|
|
1310
|
-
* The review and fix workflows' human review since `base`: a sign-off closes
|
|
1311
|
-
* `.gtd/REVIEW.md`; a note leaves it, with the reviewer's own version as
|
|
1312
|
-
* `REVIEW_ORIG`, for the next `fix` to read — never a re-plan.
|
|
1313
|
-
*/
|
|
1314
|
-
const handOffReview = (base, escalations) => reviewToHandOff(base, {
|
|
1315
|
-
file: REVIEW,
|
|
1316
|
-
reviewing,
|
|
1317
|
-
fixRisks,
|
|
1318
|
-
keepGreen: () => healthy(fixCheck, { escalations }),
|
|
1319
|
-
awaitReview: (b) => awaitReview(b, handOffAwaitReviewMessage(b)),
|
|
1320
|
-
reviewMissing,
|
|
1321
|
-
close: closeReview,
|
|
1322
|
-
handOff: (reviewed) => run("review.handing-off", showScript(reviewed, REVIEW, REVIEW_ORIG), { label: "Handing your notes to fix" }),
|
|
1323
|
-
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.",
|
|
1324
|
-
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.",
|
|
1325
|
-
deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note to hand to fix."
|
|
1326
1318
|
});
|
|
1327
|
-
/** The tail the review and fix workflows run: ends at the hand-off review since `base`. */
|
|
1328
|
-
const handOffTail = (fixFirst, base) => tail(fixFirst, (escalations) => handOffReview(base, escalations));
|
|
1329
1319
|
//#endregion
|
|
1330
1320
|
//#region src/workflows/shared/skills.ts
|
|
1331
1321
|
const skills = (vars) => ({
|
|
@@ -1346,6 +1336,9 @@ const skills = (vars) => ({
|
|
|
1346
1336
|
"build.review.fix.nits": ["incremental-implementation", "code-simplification"],
|
|
1347
1337
|
"build.review.fix.risks": ["debugging-and-error-recovery", "incremental-implementation"],
|
|
1348
1338
|
notes: ["incremental-implementation", "code-simplification"],
|
|
1339
|
+
refactor: ["ponytail"],
|
|
1340
|
+
"refactor.fix.suite": ["ponytail", "debugging-and-error-recovery"],
|
|
1341
|
+
"refactor.health": ["ponytail", "debugging-and-error-recovery"],
|
|
1349
1342
|
...lensScopes("build.quality", lensesOf(vars.qualityReviews ?? ""), (lens) => builtInLenses[lens]?.skills ?? [lens])
|
|
1350
1343
|
});
|
|
1351
1344
|
//#endregion
|
|
@@ -1369,4 +1362,4 @@ const steering = {
|
|
|
1369
1362
|
[REVIEW]: "review"
|
|
1370
1363
|
};
|
|
1371
1364
|
//#endregion
|
|
1372
|
-
export {
|
|
1365
|
+
export { reviewGateBlockedMessage as $, architectureAuthorPrompt as A, designGateAnswerMessage as B, fixRisks as C, reviewing as D, reviewQuality as E, buildReviewCollectingPrompt as F, handOffAwaitReviewMessage as G, designTriagePrompt as H, buildReviewFixNitsPrompt as I, packagesItemFixSuitePrompt as J, idleMessage as K, buildReviewTriageMessage as L, architectureGateAnswerMessage as M, buildReviewAnswerQuestionsPrompt as N, agentWithSkills as O, buildReviewAwaitAfterDeferMessage as P, reviewEditsCapture as Q, builderSystem as R, fixQuality as S, reviewMissing as T, finisherSystem as U, designSystem as V, fixReviewNotesPrompt as W, refactorFixPrompt as X, refactorBuildingPrompt as Y, reviewEditNotesCapture as Z, REQUIREMENTS as _, flagShipped as a, coder as b, envDefaults as c, gate as d, reviewNotesCapture as et, guarded as f, QUALITY as g, FEEDBACK as h, skills as i, unwindFailure as it, architectureDecomposePrompt as j, architectSystem as k, baseline as l, ARCHITECTURE as m, summary as n, startGateBlockedMessage as nt, shipped as o, healthy as p, packagesItemBuildingPrompt as q, access as r, unwindFailedMessage as rt, defaults as s, steering as t, reviewerSystem as tt, escalation as u, REVIEW as v, planner as w, fixCheck as x, awaitReview as y, deferNotesScript as z };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { read, refuse, removeScript, run, scope, showScript, vars } from "../flows.mjs";
|
|
2
|
+
import { fixFindings, lensesOf, qualityLap as qualityLap$1, reviewToHandOff } from "../workflow-kit.mjs";
|
|
3
|
+
import { C as fixRisks, D as reviewing, E as reviewQuality, G as handOffAwaitReviewMessage, S as fixQuality, T as reviewMissing, f as guarded, g as QUALITY, p as healthy, u as escalation, v as REVIEW, x as fixCheck, y as awaitReview } from "./reserved-BPdVZkgv.mjs";
|
|
4
|
+
//#region src/workflows/shared/tail.ts
|
|
5
|
+
/** The reviewer's own `.gtd/REVIEW.md`, kept beside the human's notes so `fix` can tell them apart. */
|
|
6
|
+
const REVIEW_ORIG = ".gtd/REVIEW.orig.md";
|
|
7
|
+
/**
|
|
8
|
+
* One review turn per lens over the whole change, each appending every
|
|
9
|
+
* finding to `.gtd/QUALITY.md`. Resolves `"findings"` when that file
|
|
10
|
+
* has any.
|
|
11
|
+
*/
|
|
12
|
+
const qualityLap = (guard) => qualityLap$1(lensesOf(vars.qualityReviews ?? ""), guarded(reviewQuality, guard), QUALITY);
|
|
13
|
+
/** Resolves `true` once `.gtd/QUALITY.md` is resolved, `false` when the fix cap escalated instead. */
|
|
14
|
+
const fixQualityFindings = (escalations, guard) => fixFindings({
|
|
15
|
+
fix: guarded(fixQuality, guard),
|
|
16
|
+
findings: QUALITY,
|
|
17
|
+
cap: 3,
|
|
18
|
+
escalate: () => escalation(escalations, guard)
|
|
19
|
+
});
|
|
20
|
+
/** `.gtd/REVIEW.md` removed at the end of a round at commit `round`. */
|
|
21
|
+
const closeReview = (round) => run("review.closing", removeScript([REVIEW, REVIEW_ORIG]), {
|
|
22
|
+
label: "Closing the review",
|
|
23
|
+
base: round
|
|
24
|
+
});
|
|
25
|
+
/** Refuses while a reviewer's notes still await `gtd door fix`. */
|
|
26
|
+
const refusePendingHandOff = () => {
|
|
27
|
+
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}.`);
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* The build tail: fix (when entered red), keep green, the quality lap, then
|
|
31
|
+
* `finish` — the human review, as the calling workflow runs it. `green`: the
|
|
32
|
+
* caller just left the full suite green (the package loop), so no run opens it.
|
|
33
|
+
*/
|
|
34
|
+
const tail = (fixFirst, finish, green = false, guard) => scope("build", async () => {
|
|
35
|
+
const escalations = { rounds: 0 };
|
|
36
|
+
const fix = fixCheck;
|
|
37
|
+
const repair = async () => {
|
|
38
|
+
await guarded(fix, guard)();
|
|
39
|
+
await healthy(fix, {
|
|
40
|
+
fixesSoFar: 1,
|
|
41
|
+
escalations,
|
|
42
|
+
guard
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
if (fixFirst) await repair();
|
|
46
|
+
else if (!green) await healthy(fix, {
|
|
47
|
+
escalations,
|
|
48
|
+
guard
|
|
49
|
+
});
|
|
50
|
+
if (await qualityLap(guard) === "findings") if (await fixQualityFindings(escalations, guard)) await healthy(fix, {
|
|
51
|
+
escalations,
|
|
52
|
+
guard
|
|
53
|
+
});
|
|
54
|
+
else await repair();
|
|
55
|
+
return finish(escalations);
|
|
56
|
+
});
|
|
57
|
+
/**
|
|
58
|
+
* The review and fix workflows' human review since `base`: a sign-off closes
|
|
59
|
+
* `.gtd/REVIEW.md`; a note leaves it, with the reviewer's own version as
|
|
60
|
+
* `REVIEW_ORIG`, for the next `fix` to read — never a re-plan.
|
|
61
|
+
*/
|
|
62
|
+
const handOffReview = (base, escalations, guard) => reviewToHandOff(base, {
|
|
63
|
+
file: REVIEW,
|
|
64
|
+
reviewing: guarded(reviewing, guard),
|
|
65
|
+
fixRisks: guarded(fixRisks, guard),
|
|
66
|
+
keepGreen: () => healthy(fixCheck, {
|
|
67
|
+
escalations,
|
|
68
|
+
guard
|
|
69
|
+
}),
|
|
70
|
+
awaitReview: (b) => awaitReview(b, handOffAwaitReviewMessage(b)),
|
|
71
|
+
reviewMissing,
|
|
72
|
+
close: closeReview,
|
|
73
|
+
handOff: (reviewed) => run("review.handing-off", showScript(reviewed, REVIEW, REVIEW_ORIG), { label: "Handing your notes to fix" }),
|
|
74
|
+
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.",
|
|
75
|
+
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.",
|
|
76
|
+
deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note to hand to fix."
|
|
77
|
+
});
|
|
78
|
+
/** The tail the review and fix workflows run: ends at the hand-off review since `base`. */
|
|
79
|
+
const handOffTail = (fixFirst, base, guard) => tail(fixFirst, (escalations) => handOffReview(base, escalations, guard), false, guard);
|
|
80
|
+
//#endregion
|
|
81
|
+
export { tail as a, refusePendingHandOff as i, closeReview as n, handOffTail as r, REVIEW_ORIG as t };
|
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -20964,7 +20964,6 @@ const restorability = (git, headHash, tipHash) => gen(function* () {
|
|
|
20964
20964
|
//#region src/flags/index.ts
|
|
20965
20965
|
const FLAGS$1 = [];
|
|
20966
20966
|
const BOOL = {
|
|
20967
|
-
"": false,
|
|
20968
20967
|
"0": false,
|
|
20969
20968
|
false: false,
|
|
20970
20969
|
"1": true,
|
|
@@ -20972,9 +20971,13 @@ const BOOL = {
|
|
|
20972
20971
|
};
|
|
20973
20972
|
const resolveFlags = (env, registry = FLAGS$1) => {
|
|
20974
20973
|
const out = {};
|
|
20975
|
-
for (const { name } of registry) {
|
|
20974
|
+
for (const { name, defaultOn } of registry) {
|
|
20976
20975
|
const variable = `GTD_FLAG_${name.toUpperCase()}`;
|
|
20977
20976
|
const key = (env[variable] ?? "").toLowerCase();
|
|
20977
|
+
if (key === "") {
|
|
20978
|
+
out[name] = defaultOn ?? false;
|
|
20979
|
+
continue;
|
|
20980
|
+
}
|
|
20978
20981
|
if (!Object.hasOwn(BOOL, key)) throw new Error(`${variable} must be 1, 0, true or false`);
|
|
20979
20982
|
out[name] = BOOL[key];
|
|
20980
20983
|
}
|
package/dist/testkit.mjs
CHANGED
|
@@ -6091,7 +6091,6 @@ const restorability = (git, headHash, tipHash) => gen(function* () {
|
|
|
6091
6091
|
//#region src/flags/index.ts
|
|
6092
6092
|
const FLAGS$1 = [];
|
|
6093
6093
|
const BOOL = {
|
|
6094
|
-
"": false,
|
|
6095
6094
|
"0": false,
|
|
6096
6095
|
false: false,
|
|
6097
6096
|
"1": true,
|
|
@@ -6099,9 +6098,13 @@ const BOOL = {
|
|
|
6099
6098
|
};
|
|
6100
6099
|
const resolveFlags = (env, registry = FLAGS$1) => {
|
|
6101
6100
|
const out = {};
|
|
6102
|
-
for (const { name } of registry) {
|
|
6101
|
+
for (const { name, defaultOn } of registry) {
|
|
6103
6102
|
const variable = `GTD_FLAG_${name.toUpperCase()}`;
|
|
6104
6103
|
const key = (env[variable] ?? "").toLowerCase();
|
|
6104
|
+
if (key === "") {
|
|
6105
|
+
out[name] = defaultOn ?? false;
|
|
6106
|
+
continue;
|
|
6107
|
+
}
|
|
6105
6108
|
if (!Object.hasOwn(BOOL, key)) throw new Error(`${variable} must be 1, 0, true or false`);
|
|
6106
6109
|
out[name] = BOOL[key];
|
|
6107
6110
|
}
|
|
@@ -12958,6 +12961,17 @@ const recognizeDeferNotes = (repo, block) => {
|
|
|
12958
12961
|
repo.writeFile(".gtd/TODO.md", `${old}${sep}${lines.join("\n")}\n`);
|
|
12959
12962
|
return { kind: "applied" };
|
|
12960
12963
|
};
|
|
12964
|
+
/** `mkdir -p <dir>` then `printf '%s\n' <quoted lines> > <path>`: a flow's one-file write. Re-rendered from what was extracted, so any other shape stays unrecognized. */
|
|
12965
|
+
const recognizeWriteLines = (repo, block) => {
|
|
12966
|
+
const [mkdir, printf, ...rest] = block.split("\n");
|
|
12967
|
+
if (rest.length > 0 || !mkdir?.startsWith("mkdir -p ") || !printf?.startsWith("printf '%s\\n' ")) return;
|
|
12968
|
+
const tokens = extractQuotedTokens(printf.slice(14));
|
|
12969
|
+
const path = tokens.pop();
|
|
12970
|
+
if (path === void 0 || !path.includes("/")) return void 0;
|
|
12971
|
+
if (`mkdir -p ${path.slice(0, path.lastIndexOf("/"))}\nprintf '%s\\n' ${tokens.map(shellQuote).join(" ")} > ${shellQuote(path)}` !== block) return void 0;
|
|
12972
|
+
repo.writeFile(path, `${tokens.join("\n")}\n`);
|
|
12973
|
+
return { kind: "applied" };
|
|
12974
|
+
};
|
|
12961
12975
|
/** Every recognizer `applyEmittedScript` tries, first match wins — `recognizeScriptedCommand` stays last since it's the exact-hit fallback for anything the rest don't claim. */
|
|
12962
12976
|
const recognizersFor = (repo, commands) => [
|
|
12963
12977
|
(block) => recognizeSetFlags(block),
|
|
@@ -12967,6 +12981,7 @@ const recognizersFor = (repo, commands) => [
|
|
|
12967
12981
|
(block) => recognizeBinaryGuard(commands, block),
|
|
12968
12982
|
(block) => recognizeGitBuilders(repo, block),
|
|
12969
12983
|
(block) => recognizeDeferNotes(repo, block),
|
|
12984
|
+
(block) => recognizeWriteLines(repo, block),
|
|
12970
12985
|
(block) => recognizeFailurePromptWrapper(repo, commands, block),
|
|
12971
12986
|
(block) => recognizePresentationSubshell(repo, commands, block),
|
|
12972
12987
|
(block) => recognizeModeContradictionSkipNotice(block),
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
|
|
2
|
+
import { a as flagShipped, b as coder, c as envDefaults, i as skills, n as summary, r as access, s as defaults, t as steering } from "../chunks/reserved-BPdVZkgv.mjs";
|
|
3
|
+
import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-iLAMoigl.mjs";
|
|
4
|
+
import { i as requireFlag, r as registryEntry, t as flagReferences } from "../chunks/registry-CP3YuV9j.mjs";
|
|
5
|
+
//#region src/workflows/cleanup/cleanup.ts
|
|
6
|
+
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.`;
|
|
7
|
+
const cleanupFlow = async () => {
|
|
8
|
+
const registry = vars.flagRegistry ?? "";
|
|
9
|
+
const name = vars.flag ?? "";
|
|
10
|
+
refusePendingHandOff();
|
|
11
|
+
requireFlag("cleanup", registry, name, true);
|
|
12
|
+
let scanned = false;
|
|
13
|
+
const guard = () => {
|
|
14
|
+
const turn = scanned ? changes() : void 0;
|
|
15
|
+
if ((turn === void 0 || turn.some((c) => c.path === registry)) && registryEntry(read(registry) ?? "", name) !== void 0) refuse(`gtd land: cleanup: ${registry} still declares flag "${name}"`);
|
|
16
|
+
const hits = flagReferences((turn === void 0 ? [...glob("src/**/*.ts"), ...glob("src/**/*.tsx")] : turn.filter((c) => c.status !== "deleted").map((c) => c.path).filter((path) => path.startsWith("src/") && /\.tsx?$/.test(path))).map((path) => ({
|
|
17
|
+
path,
|
|
18
|
+
text: read(path) ?? ""
|
|
19
|
+
})), name);
|
|
20
|
+
if (hits.length > 0) refuse(`gtd land: cleanup: ${hits.join(", ")} still names GTD_FLAG_${name.toUpperCase()}`);
|
|
21
|
+
};
|
|
22
|
+
await scope("cleanup", async () => {
|
|
23
|
+
await agent("removing", removingPrompt(name, registry), {
|
|
24
|
+
label: `Removing ${name}`,
|
|
25
|
+
model: coder()
|
|
26
|
+
});
|
|
27
|
+
guard();
|
|
28
|
+
scanned = true;
|
|
29
|
+
});
|
|
30
|
+
await handOffTail(false, start(), guard);
|
|
31
|
+
};
|
|
32
|
+
const cleanup = flagShipped(cleanupFlow);
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/workflows/cleanup/index.ts
|
|
35
|
+
var cleanup_default = cleanup;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { access, cleanup, cleanupFlow, cleanup_default as default, defaults, envDefaults, skills, steering, summary };
|