@pmelab/gtd 24.3.0 → 25.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 +28 -22
- package/dist/chunks/{reserved-DJwU5C4z.mjs → reserved-6pAgFGgB.mjs} +119 -135
- package/dist/chunks/{steering-D-PvRC5W.mjs → steering-DVILanVf.mjs} +16 -5
- package/dist/flows.mjs +9 -1
- package/dist/gtd.bundle.mjs +780 -813
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +666 -161
- package/dist/workflow-kit.mjs +55 -96
- package/dist/workflows/feature.mjs +171 -76
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +1 -1
- package/dist/workflows.mjs +353 -310
- package/package.json +1 -2
- package/src/cli/Program.test.ts +3 -8
- package/src/cli/Program.ts +1 -8
- package/src/edge/Edge.test.ts +5 -2
- package/src/edge/Edge.ts +3 -4
- package/src/emit/index.ts +1 -0
- package/src/flows/scripts.ts +6 -0
- package/src/platform/Git.test.ts +6 -9
- package/src/replay/Replay.ts +2 -1
- package/src/steering/FORMATS.md +1 -1
- package/src/steering/SteeringFormat.test.ts +9 -1
- package/src/steering/SteeringFormat.ts +7 -0
- package/src/steering/contract.test.ts +3 -0
- package/src/steering/index.ts +1 -0
- package/src/steering/review.test.ts +30 -0
- package/src/steering/review.ts +8 -2
- package/src/testkit/EmittedScriptRecognizer.test.ts +29 -30
- package/src/testkit/EmittedScriptRecognizer.ts +148 -51
- package/src/testkit/EmittedScripts.test.ts +166 -0
- package/src/testkit/FlowScripts.test.ts +242 -0
- package/src/testkit/FlowScripts.ts +326 -0
- package/src/testkit/GitTiers.ts +140 -49
- package/src/testkit/InMemRepo.ts +35 -2
- package/src/testkit/Layers.ts +10 -2
- package/src/testkit/TestRepo.ts +4 -1
- package/src/testkit/index.ts +1 -1
- package/src/workflow/discovery.test.ts +147 -0
- package/src/workflow/discovery.ts +5 -0
- package/src/workflow/fingerprint.test.ts +20 -4
- package/src/workflow/fingerprint.ts +8 -9
- package/src/workflow/index.ts +1 -6
- package/src/workflow/load.test.ts +5 -19
- package/src/workflow/load.ts +94 -10
- package/src/workflow-kit/review.test.ts +84 -0
- package/src/workflow-kit/review.ts +9 -6
- package/src/workflow-kit/scenarios.test.ts +116 -0
- package/src/workflow-kit/scenarios.ts +53 -113
- package/src/workflows/feature/feature.ts +2 -5
- package/src/workflows/feature/packages.test.ts +17 -34
- package/src/workflows/feature/packages.ts +13 -52
- package/src/workflows/feature/planning.ts +1 -1
- package/src/workflows/feature/review.test.ts +233 -54
- package/src/workflows/feature/review.ts +178 -41
- package/src/workflows/feature/steps.test.ts +12 -0
- package/src/workflows/feature/steps.ts +8 -1
- package/src/workflows/index.ts +0 -2
- package/src/workflows/shared/access.test.ts +4 -2
- package/src/workflows/shared/access.ts +1 -0
- package/src/workflows/shared/health.test.ts +1 -21
- package/src/workflows/shared/health.ts +4 -39
- package/src/workflows/shared/options.test.ts +8 -0
- package/src/workflows/shared/options.ts +1 -1
- package/src/workflows/shared/steps.ts +18 -11
- package/src/workflows/shared/tail.test.ts +1 -41
- package/src/workflows/shared/tail.ts +17 -41
- package/src/workflows/shared/text.fixture.ts +1 -0
- package/src/workflows/shared/text.test.ts +129 -41
- package/src/workflows/shared/text.ts +111 -75
- package/src/workflows/shared/vars.ts +1 -1
- package/src/workflows/shared/scenarios.test.ts +0 -256
- package/src/workflows/shared/scenarios.ts +0 -35
package/dist/workflow-kit.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { changes, changesSince, hasThreadFor, head, read, refuse, requireReplies, requireThreadsClosed, scope } from "./flows.mjs";
|
|
2
|
-
import { c as stripCodeThreads, d as reviewNotes, f as reviewRisks } from "./chunks/steering-
|
|
2
|
+
import { c as stripCodeThreads, d as reviewNotes, f as reviewRisks } from "./chunks/steering-DVILanVf.mjs";
|
|
3
3
|
//#region src/workflow-kit/health.ts
|
|
4
4
|
/** Hand a red suite that repeated attempts could not fix to a person, until `limit` rounds are spent. */
|
|
5
5
|
const escalation = async (count, steps) => {
|
|
@@ -67,102 +67,59 @@ const fixFindings = async (f) => {
|
|
|
67
67
|
};
|
|
68
68
|
//#endregion
|
|
69
69
|
//#region src/workflow-kit/scenarios.ts
|
|
70
|
-
const DOC_STRING = /^(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const scenarioText = (source) => {
|
|
70
|
+
const DOC_STRING = /^("""|```)/;
|
|
71
|
+
const THEN = /^Then\s/;
|
|
72
|
+
const CONTINUATION = /^(And|But|\*)\s/;
|
|
73
|
+
const HEADING = /^(Feature|Rule|Background|Scenario|Scenario Outline|Scenario Template|Example):/;
|
|
74
|
+
/** Trimmed lines outside doc strings, whose content is test input, not steps. */
|
|
75
|
+
const stepLines = (source) => {
|
|
77
76
|
const out = [];
|
|
78
|
-
let
|
|
79
|
-
for (const
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
fence: delimiter[2]
|
|
85
|
-
};
|
|
86
|
-
out.push(line.trim());
|
|
87
|
-
} else if (doc !== void 0 && line.trim().startsWith(doc.fence)) {
|
|
88
|
-
doc = void 0;
|
|
89
|
-
out.push(line.trim());
|
|
90
|
-
} else if (doc !== void 0) {
|
|
91
|
-
const lead = line.length - line.trimStart().length;
|
|
92
|
-
out.push(line.slice(Math.min(lead, doc.indent)).trimEnd());
|
|
93
|
-
} else {
|
|
94
|
-
const trimmed = line.trim();
|
|
95
|
-
if (trimmed !== "" && !trimmed.startsWith("#") && !trimmed.startsWith("@")) out.push(trimmed);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return out.join("\n");
|
|
99
|
-
};
|
|
100
|
-
const textOf = (path) => {
|
|
101
|
-
const source = read(path);
|
|
102
|
-
return source === void 0 ? void 0 : scenarioText(source);
|
|
103
|
-
};
|
|
104
|
-
/** Snapshot `.feature` paths at `head()`, merged over `previous`; `undefined` when nothing is frozen. */
|
|
105
|
-
const freezeScenarios = (paths, previous) => {
|
|
106
|
-
if (paths.length === 0) return previous;
|
|
107
|
-
const texts = { ...previous?.texts };
|
|
108
|
-
for (const path of paths) {
|
|
109
|
-
const text = textOf(path);
|
|
110
|
-
if (text !== void 0) texts[path] = text;
|
|
77
|
+
let fence;
|
|
78
|
+
for (const trimmed of source.split("\n").map((l) => l.trim())) {
|
|
79
|
+
const opening = fence === void 0 ? DOC_STRING.exec(trimmed)?.[1] : void 0;
|
|
80
|
+
if (opening !== void 0) fence = opening;
|
|
81
|
+
else if (fence === void 0) out.push(trimmed);
|
|
82
|
+
else if (trimmed.startsWith(fence)) fence = void 0;
|
|
111
83
|
}
|
|
112
|
-
return
|
|
113
|
-
at: head(),
|
|
114
|
-
texts
|
|
115
|
-
};
|
|
116
|
-
};
|
|
117
|
-
/** Paths whose wording differs from the snapshot (a deleted file counts). */
|
|
118
|
-
const driftedScenarios = (frozen) => Object.entries(frozen.texts).filter(([path, text]) => textOf(path) !== text).map(([path]) => path);
|
|
119
|
-
const lines = (text) => text === void 0 || text === "" ? [] : text.split("\n");
|
|
120
|
-
/** `table[i][j]`: the longest common subsequence of `a[i..]` and `b[j..]`. */
|
|
121
|
-
const lcsTable = (a, b) => {
|
|
122
|
-
const table = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
123
|
-
for (let i = a.length - 1; i >= 0; i--) for (let j = b.length - 1; j >= 0; j--) table[i][j] = a[i] === b[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);
|
|
124
|
-
return table;
|
|
84
|
+
return out;
|
|
125
85
|
};
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
86
|
+
const neutral = (line) => line === "" || line.startsWith("#") || line.startsWith("|");
|
|
87
|
+
/** Trimmed `Then` step lines, an `And`/`But`/`*` continuing one included; doc strings skipped. */
|
|
88
|
+
const assertionLines = (source) => {
|
|
129
89
|
const out = [];
|
|
130
|
-
let
|
|
131
|
-
let
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
90
|
+
let inThen = false;
|
|
91
|
+
let scenario = "";
|
|
92
|
+
for (const line of stepLines(source)) {
|
|
93
|
+
if (HEADING.test(line)) scenario = line;
|
|
94
|
+
if (THEN.test(line)) inThen = true;
|
|
95
|
+
else if (!CONTINUATION.test(line) && !neutral(line)) inThen = false;
|
|
96
|
+
if (inThen && !neutral(line)) out.push({
|
|
97
|
+
scenario,
|
|
98
|
+
line
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
142
102
|
};
|
|
143
|
-
const
|
|
144
|
-
path,
|
|
145
|
-
lines: lineDiff(lines(frozen.texts[path]), lines(textOf(path)))
|
|
146
|
-
});
|
|
103
|
+
const key = (a) => `${a.scenario}\n${a.line}`;
|
|
147
104
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
105
|
+
* Assertions a `.feature` file that existed before `changes` lost — the one
|
|
106
|
+
* way rewording a scenario can weaken it. Matched within each scenario and
|
|
107
|
+
* counted, so a line moving to another scenario, or one of two identical
|
|
108
|
+
* `Then`s dropped, still shows. A renamed scenario reports all of its own.
|
|
151
109
|
*/
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
};
|
|
110
|
+
const removedAssertions = (changes) => changes.filter((c) => c.path.endsWith(".feature") && c.before !== void 0).map((c) => {
|
|
111
|
+
const left = /* @__PURE__ */ new Map();
|
|
112
|
+
for (const a of assertionLines(c.after ?? "")) left.set(key(a), (left.get(key(a)) ?? 0) + 1);
|
|
113
|
+
const lines = assertionLines(c.before ?? "").filter((a) => {
|
|
114
|
+
const n = left.get(key(a)) ?? 0;
|
|
115
|
+
left.set(key(a), n - 1);
|
|
116
|
+
return n <= 0;
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
path: c.path,
|
|
120
|
+
lines
|
|
121
|
+
};
|
|
122
|
+
}).filter((r) => r.lines.length > 0);
|
|
166
123
|
//#endregion
|
|
167
124
|
//#region src/workflow-kit/review.ts
|
|
168
125
|
const collect = async (p, capture) => {
|
|
@@ -196,16 +153,17 @@ const converse = async (p, base, collectedBefore) => {
|
|
|
196
153
|
if (!hasThreadFor("human", p.file)) return collectedAt;
|
|
197
154
|
}
|
|
198
155
|
};
|
|
199
|
-
/** Judge every note, then run each verdict's route: answers, then nit fixes, then the edits' lap. */
|
|
156
|
+
/** Judge every note, then run each verdict's route: answers, then nit fixes, then deferrals, then the edits' lap. */
|
|
200
157
|
const routeNotes = async (p, notes, r) => {
|
|
201
158
|
const verdicts = await p.triage(notes);
|
|
202
159
|
const of = (v) => notes.filter((n) => (verdicts.get(n.id) ?? "edit") === v);
|
|
203
|
-
const [edits, questions, nits] = [
|
|
160
|
+
const [edits, questions, nits, deferred] = [
|
|
204
161
|
of("edit"),
|
|
205
162
|
of("question"),
|
|
206
|
-
of("nit")
|
|
163
|
+
of("nit"),
|
|
164
|
+
of("defer")
|
|
207
165
|
];
|
|
208
|
-
if (edits.length + questions.length + nits.length === 0) return r.unfolded();
|
|
166
|
+
if (edits.length + questions.length + nits.length + deferred.length === 0) return r.unfolded();
|
|
209
167
|
let answeredAt;
|
|
210
168
|
if (questions.length > 0) {
|
|
211
169
|
await p.answerQuestions(questions);
|
|
@@ -215,6 +173,7 @@ const routeNotes = async (p, notes, r) => {
|
|
|
215
173
|
await p.fixNits(nits);
|
|
216
174
|
await p.keepGreen();
|
|
217
175
|
}
|
|
176
|
+
if (deferred.length > 0) await p.defer(deferred);
|
|
218
177
|
if (edits.length > 0) {
|
|
219
178
|
await r.close();
|
|
220
179
|
return r.outcome(await collect(p, p.captures.editNotes(r.round, edits, answeredAt)));
|
|
@@ -228,7 +187,7 @@ const routeNotes = async (p, notes, r) => {
|
|
|
228
187
|
}
|
|
229
188
|
return {
|
|
230
189
|
next: "await",
|
|
231
|
-
reviewed: answeredAt
|
|
190
|
+
reviewed: answeredAt ?? r.round
|
|
232
191
|
};
|
|
233
192
|
};
|
|
234
193
|
/** A code edit, not a thread-only change: lines that only add, answer or remove code threads don't count. */
|
|
@@ -349,4 +308,4 @@ const reviewToHandOff = async (base, p) => {
|
|
|
349
308
|
}
|
|
350
309
|
};
|
|
351
310
|
//#endregion
|
|
352
|
-
export {
|
|
311
|
+
export { assertionLines, escalation, fixFindings, gate, healthy, lensScopes, lensesOf, qualityLap, removedAssertions, review, reviewToHandOff, routeNotes, untilSettled };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { answered, changes, changesSince, glob, hasThreadFor, head, human, judge, numeric, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, start, vars } from "../flows.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { $ as
|
|
2
|
+
import { review as review$1 } from "../workflow-kit.mjs";
|
|
3
|
+
import { $ as unwindFailedMessage, A as architectureDecomposePrompt, B as designSystem, C as fixRisks, D as agentWithSkills, E as reviewing, F as buildReviewFixNitsPrompt, G as packagesItemBuildingPrompt, I as buildReviewTriageMessage, J as reviewEditsCapture, K as packagesItemFixSuitePrompt, L as builderSystem, M as buildReviewAnswerQuestionsPrompt, N as buildReviewAwaitAfterDeferMessage, O as architectSystem, P as buildReviewCollectingPrompt, Q as startGateBlockedMessage, R as deferNotesScript, S as fixCheck, T as reviewMissing, V as designTriagePrompt, W as idleMessage, X as reviewNotesCapture, Z as reviewerSystem, _ as FEEDBACK, b as awaitReview, c as refusePendingHandOff, d as defaults, et as unwindFailure, f as envDefaults, g as ARCHITECTURE, h as healthy, i as skills, j as architectureGateAnswerMessage, k as architectureAuthorPrompt, l as tail, m as gate, n as summary, o as closeReview, q as reviewEditNotesCapture, r as access, t as steering, u as shipped, v as REQUIREMENTS, w as planner, x as coder, y as REVIEW, z as designGateAnswerMessage } from "../chunks/reserved-6pAgFGgB.mjs";
|
|
4
4
|
//#region src/workflows/feature/steps.ts
|
|
5
5
|
/** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
|
|
6
6
|
const triage = (base) => agentWithSkills("triage", designTriagePrompt(base), {
|
|
@@ -71,10 +71,13 @@ const collecting = (capture) => agentWithSkills("review.collecting", buildReview
|
|
|
71
71
|
system: reviewerSystem(),
|
|
72
72
|
allowEmpty: true
|
|
73
73
|
});
|
|
74
|
+
/** `build.review.defer`: append `notes` to `.gtd/TODO.md` as one section; the step's landing is the commit. */
|
|
75
|
+
const deferNotes = (notes, round) => run("review.defer", deferNotesScript(notes, round), {
|
|
76
|
+
label: "Deferring could-notes to TODO.md",
|
|
77
|
+
file: ".gtd/TODO.md"
|
|
78
|
+
});
|
|
74
79
|
//#endregion
|
|
75
80
|
//#region src/workflows/feature/packages.ts
|
|
76
|
-
/** Package 0: the e2e scenarios, written red. */
|
|
77
|
-
const SCENARIO_PACKAGE = ".gtd/packages/00-e2e-scenarios.md";
|
|
78
81
|
const TEST_LINE = /^\s*-\s+(unit|e2e):\s+`([^`]+)`/;
|
|
79
82
|
/** The `## Tests` entries of a package file; malformed lines are ignored. */
|
|
80
83
|
const declaredTests = (packageText) => {
|
|
@@ -104,47 +107,29 @@ const requireDeclaredTests = (pkg, since) => {
|
|
|
104
107
|
if (missing.length === 0) return;
|
|
105
108
|
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")}`);
|
|
106
109
|
};
|
|
107
|
-
/**
|
|
108
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Build `pkg`, check its declared tests, keep the full suite green, and close
|
|
112
|
+
* it out, which removes it — and `.gtd/ARCHITECTURE.md` with the last one.
|
|
113
|
+
*/
|
|
114
|
+
const packageItem = async (pkg) => {
|
|
109
115
|
const from = head();
|
|
110
116
|
const declared = (turn) => async () => {
|
|
111
117
|
await turn();
|
|
112
118
|
requireDeclaredTests(pkg, from);
|
|
113
119
|
};
|
|
114
|
-
await
|
|
115
|
-
await healthy(
|
|
116
|
-
await run("closing", removeScript([
|
|
117
|
-
return {
|
|
120
|
+
await declared(() => build(pkg))();
|
|
121
|
+
await healthy(declared(fixSuite));
|
|
122
|
+
await run("closing", removeScript([
|
|
118
123
|
pkg,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
};
|
|
124
|
+
".gtd/SATISFIED.md",
|
|
125
|
+
...glob(".gtd/packages/*.md").length === 1 ? [ARCHITECTURE] : []
|
|
126
|
+
]), { label: "Closing out the package" });
|
|
122
127
|
};
|
|
123
128
|
/** The first queued package — the one a package step works on. */
|
|
124
129
|
const nextPackage = () => [...glob(".gtd/packages/*.md")].sort()[0];
|
|
125
|
-
const outsideGtd = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
126
130
|
/** Build every package file under `.gtd/packages/`, in name order. */
|
|
127
|
-
const packages = (
|
|
128
|
-
let
|
|
129
|
-
const ranges = [];
|
|
130
|
-
let touched = [];
|
|
131
|
-
for (let pkg = nextPackage(); pkg !== void 0; pkg = nextPackage()) {
|
|
132
|
-
const range = await scope("item", () => packageItem(pkg, pkg === ".gtd/packages/00-e2e-scenarios.md" ? void 0 : frozen));
|
|
133
|
-
ranges.push(range);
|
|
134
|
-
if (pkg === ".gtd/packages/00-e2e-scenarios.md") {
|
|
135
|
-
touched = changesSince(range.from).filter((c) => outsideGtd(c.path) && c.status !== "deleted");
|
|
136
|
-
frozen = freezeScenarios(touched.filter((c) => c.path.endsWith(".feature")).map((c) => c.path), frozen);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
const paths = (status) => touched.filter((c) => c.status === status).map((c) => c.path);
|
|
140
|
-
return {
|
|
141
|
-
ranges,
|
|
142
|
-
scenarios: {
|
|
143
|
-
added: paths("added"),
|
|
144
|
-
changed: paths("modified")
|
|
145
|
-
},
|
|
146
|
-
frozen
|
|
147
|
-
};
|
|
131
|
+
const packages = () => scope("packages", async () => {
|
|
132
|
+
for (let pkg = nextPackage(); pkg !== void 0; pkg = nextPackage()) await scope("item", () => packageItem(pkg));
|
|
148
133
|
});
|
|
149
134
|
//#endregion
|
|
150
135
|
//#region src/workflows/feature/planning.ts
|
|
@@ -177,7 +162,7 @@ const architecture = () => scope("architecture", async () => {
|
|
|
177
162
|
} while (await noteGate(ARCHITECTURE, answerTechnicalQuestions));
|
|
178
163
|
await decompose();
|
|
179
164
|
if (changes(".gtd/packages/**").length === 0) refuse("gtd land: decompose: write at least one package under .gtd/packages/");
|
|
180
|
-
if (changes(".gtd/ARCHITECTURE.md").some((c) => c.status === "deleted")) refuse("gtd land: decompose: keep .gtd/ARCHITECTURE.md — the
|
|
165
|
+
if (changes(".gtd/ARCHITECTURE.md").some((c) => c.status === "deleted")) refuse("gtd land: decompose: keep .gtd/ARCHITECTURE.md — the packages are built from it; it is removed with the last package");
|
|
181
166
|
});
|
|
182
167
|
//#endregion
|
|
183
168
|
//#region src/workflows/feature/review.ts
|
|
@@ -194,23 +179,106 @@ const verdictQuestion = (note) => ({
|
|
|
194
179
|
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.`,
|
|
195
180
|
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."
|
|
196
181
|
});
|
|
182
|
+
const SEVERITY_ANSWERS = {
|
|
183
|
+
security: ["yes", "no"],
|
|
184
|
+
reversibility: ["one-way", "two-way"],
|
|
185
|
+
blastRadius: [
|
|
186
|
+
"local",
|
|
187
|
+
"module",
|
|
188
|
+
"contract"
|
|
189
|
+
],
|
|
190
|
+
newConcept: ["yes", "no"]
|
|
191
|
+
};
|
|
197
192
|
/**
|
|
198
|
-
*
|
|
193
|
+
* Deferring is the risky direction: anything unanswered, unrecognised, cut or
|
|
194
|
+
* below `minP` is `critical`, never `could`.
|
|
195
|
+
*/
|
|
196
|
+
const severityOf = (note, judgment, minP) => {
|
|
197
|
+
const ans = (q) => judgment.answers[`${note.id}.${q}`];
|
|
198
|
+
const a = (q) => ans(q)?.answer;
|
|
199
|
+
const rank = () => a("reversibility") === "one-way" ? 0 : a("newConcept") === "yes" ? 1 : 2;
|
|
200
|
+
if (note.forced) return {
|
|
201
|
+
tier: "critical",
|
|
202
|
+
reason: "critical: forced by `!`",
|
|
203
|
+
rank: 2
|
|
204
|
+
};
|
|
205
|
+
if (judgment.truncated.includes(note.id) || Object.entries(SEVERITY_ANSWERS).some(([q, allowed]) => !allowed.includes(a(q) ?? "") || !((ans(q)?.p ?? NaN) >= minP))) return {
|
|
206
|
+
tier: "critical",
|
|
207
|
+
reason: "critical: judge unsure",
|
|
208
|
+
rank: 2
|
|
209
|
+
};
|
|
210
|
+
const critical = [...a("security") === "yes" ? ["security"] : [], ...a("reversibility") === "one-way" ? ["one-way door"] : []];
|
|
211
|
+
if (critical.length > 0) return {
|
|
212
|
+
tier: "critical",
|
|
213
|
+
reason: `critical: ${critical.join(", ")}`,
|
|
214
|
+
rank: rank()
|
|
215
|
+
};
|
|
216
|
+
const should = [...a("blastRadius") === "contract" ? ["contract"] : [], ...a("newConcept") === "yes" ? ["new concept"] : []];
|
|
217
|
+
if (should.length > 0) return {
|
|
218
|
+
tier: "should",
|
|
219
|
+
reason: `should: ${should.join(", ")}`,
|
|
220
|
+
rank: rank()
|
|
221
|
+
};
|
|
222
|
+
return {
|
|
223
|
+
tier: "could",
|
|
224
|
+
reason: "could",
|
|
225
|
+
rank: 2
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
const severityQuestions = (note) => [
|
|
229
|
+
{
|
|
230
|
+
id: `${note.id}.security`,
|
|
231
|
+
primitive: "noul",
|
|
232
|
+
instructions: `Does the change the human asks for in note "${note.id}" (on "${note.anchor}") touch security — authentication, authorisation, secrets, untrusted input or data exposure?`,
|
|
233
|
+
criteria: "yes: it changes or could weaken a security property. no: it does not."
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: `${note.id}.reversibility`,
|
|
237
|
+
primitive: "choice",
|
|
238
|
+
instructions: `Once the change in note "${note.id}" ships, can it be undone without breaking anyone who already depends on it?`,
|
|
239
|
+
criteria: "one-way: a published API, file format, persisted data, protocol or other contract that cannot be taken back. two-way: can be reverted freely."
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: `${note.id}.blastRadius`,
|
|
243
|
+
primitive: "choice",
|
|
244
|
+
instructions: `How far does the change in note "${note.id}" reach?`,
|
|
245
|
+
criteria: "local: one function or file. module: a whole module or several files. contract: a public interface other code or users depend on."
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
id: `${note.id}.newConcept`,
|
|
249
|
+
primitive: "noul",
|
|
250
|
+
instructions: `Does the change in note "${note.id}" introduce a concept new to this codebase — a new abstraction, entity or term? Judge from the evidence, never a glossary; reusing or extending an existing concept is not new.`,
|
|
251
|
+
criteria: "yes: a genuinely new concept. no: reuses or extends an existing one."
|
|
252
|
+
}
|
|
253
|
+
];
|
|
254
|
+
/**
|
|
255
|
+
* One judge rest: a verdict per unforced note (a forced note is asked
|
|
256
|
+
* nothing) plus four severity answers. Dismissing a note is the risky
|
|
199
257
|
* direction, so only a confident non-`edit` verdict on uncut evidence counts;
|
|
200
|
-
* a cut, unanswered or below-floor note is an `edit
|
|
201
|
-
*
|
|
258
|
+
* a cut, unanswered or below-floor note is an `edit`. Severity answers on a
|
|
259
|
+
* non-`edit` note are ignored. A confident `could` edit becomes `defer`.
|
|
202
260
|
*/
|
|
203
|
-
const triage$1 = async (notes) => {
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
261
|
+
const triage$1 = (severities) => async (notes) => {
|
|
262
|
+
const asked = notes.filter((n) => !n.forced);
|
|
263
|
+
const judgment = asked.length === 0 ? {
|
|
264
|
+
answers: {},
|
|
265
|
+
truncated: []
|
|
266
|
+
} : await judge("review.triage", {
|
|
267
|
+
questions: asked.flatMap((n) => [verdictQuestion(n), ...severityQuestions(n)]),
|
|
268
|
+
evidence: Object.fromEntries(asked.map((n) => [n.id, `Anchor: ${n.anchor}\nReviewer's text: ${n.before}\nHuman's text: ${n.text}`])),
|
|
207
269
|
message: buildReviewTriageMessage(),
|
|
208
270
|
label: "Judging each note"
|
|
209
271
|
});
|
|
272
|
+
const { answers, truncated } = judgment;
|
|
210
273
|
const minP = numeric(vars.reviewNoteActionable, Infinity);
|
|
274
|
+
const severityMinP = numeric(vars.reviewSeverityMinP, Infinity);
|
|
275
|
+
severities.clear();
|
|
211
276
|
return new Map(notes.map((n) => {
|
|
212
|
-
const verdict = VERDICTS.find((v) => v !== "edit" && answered(answers[n.id], v, minP));
|
|
213
|
-
|
|
277
|
+
const verdict = n.forced ? void 0 : VERDICTS.find((v) => v !== "edit" && answered(answers[n.id], v, minP));
|
|
278
|
+
if (verdict !== void 0 && !truncated.includes(n.id)) return [n.id, verdict];
|
|
279
|
+
const severity = severityOf(n, judgment, severityMinP);
|
|
280
|
+
severities.set(n.id, severity);
|
|
281
|
+
return [n.id, severity.tier === "could" ? "defer" : "edit"];
|
|
214
282
|
}));
|
|
215
283
|
};
|
|
216
284
|
const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
@@ -220,32 +288,61 @@ const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
|
220
288
|
* another planning lap, questions are answered in place, nits fixed in one
|
|
221
289
|
* batch, praise dropped.
|
|
222
290
|
*/
|
|
223
|
-
const review = (base, escalations = { rounds: 0 }
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
291
|
+
const review = (base, escalations = { rounds: 0 }) => {
|
|
292
|
+
const severities = /* @__PURE__ */ new Map();
|
|
293
|
+
let deferred = [];
|
|
294
|
+
return review$1(base, {
|
|
295
|
+
file: REVIEW,
|
|
296
|
+
feedback: REQUIREMENTS,
|
|
297
|
+
isCode,
|
|
298
|
+
reviewing: (b, carry) => {
|
|
299
|
+
deferred = [];
|
|
300
|
+
return reviewing(b, carry);
|
|
301
|
+
},
|
|
302
|
+
fixRisks,
|
|
303
|
+
keepGreen: () => healthy(fixCheck, { escalations }),
|
|
304
|
+
awaitReview: (b) => {
|
|
305
|
+
const notes = deferred;
|
|
306
|
+
deferred = [];
|
|
307
|
+
return notes.length > 0 ? awaitReview(b, buildReviewAwaitAfterDeferMessage(b, notes), true) : awaitReview(b);
|
|
308
|
+
},
|
|
309
|
+
reviewMissing,
|
|
310
|
+
collecting,
|
|
311
|
+
triage: triage$1(severities),
|
|
312
|
+
defer: (notes) => {
|
|
313
|
+
deferred = notes.map((n) => ({
|
|
314
|
+
id: n.id,
|
|
315
|
+
anchor: n.anchor,
|
|
316
|
+
text: n.text,
|
|
317
|
+
label: severities.get(n.id).reason
|
|
318
|
+
}));
|
|
319
|
+
return deferNotes(deferred, head());
|
|
320
|
+
},
|
|
321
|
+
answerQuestions: answerReviewQuestions,
|
|
322
|
+
fixNits,
|
|
323
|
+
close: closeReview,
|
|
324
|
+
captures: {
|
|
325
|
+
edits: reviewEditsCapture,
|
|
326
|
+
notes: reviewNotesCapture,
|
|
327
|
+
editNotes: (round, edits, answeredAt) => reviewEditNotesCapture(round, edits.map((n, i) => ({
|
|
328
|
+
n,
|
|
329
|
+
i,
|
|
330
|
+
s: severities.get(n.id)
|
|
331
|
+
})).sort((x, y) => Number(x.s?.tier === "should") - Number(y.s?.tier === "should") || (x.s?.rank ?? 2) - (y.s?.rank ?? 2) || x.i - y.i).map(({ n, s }) => ({
|
|
332
|
+
id: n.id,
|
|
333
|
+
anchor: n.anchor,
|
|
334
|
+
text: n.forced ? n.text.replace(/^!\s*/, "") : n.text,
|
|
335
|
+
...s === void 0 ? {} : { label: s.reason }
|
|
336
|
+
})), answeredAt)
|
|
337
|
+
},
|
|
338
|
+
refusals: {
|
|
339
|
+
deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note (or edit code) to request changes.",
|
|
340
|
+
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"
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
};
|
|
344
|
+
/** The build tail: fix (when entered red), keep green unless `green`, the quality lap, then human review since `base`. */
|
|
345
|
+
const buildTail = (fixFirst, base, green = false) => tail(fixFirst, (escalations) => review(base, escalations), green);
|
|
249
346
|
//#endregion
|
|
250
347
|
//#region src/workflows/feature/feature.ts
|
|
251
348
|
/** Revert the sketch that started the process out of the working tree; its intent survives in history. */
|
|
@@ -272,13 +369,11 @@ const reUnwind = async (feedback) => {
|
|
|
272
369
|
/** Plan, build and review until a review round signs off; feedback re-plans from scratch. */
|
|
273
370
|
const planAndBuild = async (firstBase) => {
|
|
274
371
|
let base = firstBase;
|
|
275
|
-
let carried;
|
|
276
372
|
for (;;) {
|
|
277
373
|
await design(base);
|
|
278
374
|
await architecture();
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const outcome = await buildTail(false, base, built);
|
|
375
|
+
await packages();
|
|
376
|
+
const outcome = await buildTail(false, base, true);
|
|
282
377
|
if (outcome.verdict === "signoff") return;
|
|
283
378
|
await reUnwind(outcome);
|
|
284
379
|
base = outcome.base;
|
|
@@ -312,4 +407,4 @@ const feature = shipped(ordinaryStart);
|
|
|
312
407
|
//#region src/workflows/feature/index.ts
|
|
313
408
|
var feature_default = feature;
|
|
314
409
|
//#endregion
|
|
315
|
-
export {
|
|
410
|
+
export { access, afterTail, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, build, buildTail, collecting, declaredTests, decompose, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, fixSuite, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, reUnwind, requireDeclaredTests, skills, steering, summary, triage, unwind };
|
package/dist/workflows/fix.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
|
|
2
|
-
import { d as reviewNotes, f as reviewRisks } from "../chunks/steering-
|
|
3
|
-
import {
|
|
2
|
+
import { d as reviewNotes, f as reviewRisks } from "../chunks/steering-DVILanVf.mjs";
|
|
3
|
+
import { D as agentWithSkills, H as finisherSystem, U as fixReviewNotesPrompt, a as REVIEW_ORIG, d as defaults, f as envDefaults, i as skills, n as summary, p as baseline, r as access, s as handOffTail, t as steering, u as shipped, x as coder, y as REVIEW } from "../chunks/reserved-6pAgFGgB.mjs";
|
|
4
4
|
//#region src/workflows/fix/fix.ts
|
|
5
5
|
/**
|
|
6
6
|
* Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { start } from "../flows.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { Y as reviewGateBlockedMessage, c as refusePendingHandOff, d as defaults, f as envDefaults, i as skills, m as gate, n as summary, r as access, s as handOffTail, t as steering, u as shipped } from "../chunks/reserved-6pAgFGgB.mjs";
|
|
3
3
|
//#region src/workflows/review/review.ts
|
|
4
4
|
/**
|
|
5
5
|
* Pure review of everything since `reviewBase`: a green gate, the quality
|