@pmelab/gtd 23.2.0 → 24.1.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 +44 -11
- package/claude/hooks/access.ts +1 -1
- package/dist/chunks/Schema-Dh-mkwkq.mjs +25353 -0
- package/dist/chunks/reserved-ejn-LVYO.mjs +1388 -0
- package/dist/chunks/rolldown-runtime-8H4AJuhK.mjs +14 -0
- package/dist/chunks/steering-D-PvRC5W.mjs +16296 -0
- package/dist/chunks/wire-DA7q2ikx.mjs +473 -0
- package/dist/flows.mjs +525 -0
- package/dist/gtd.bundle.mjs +43830 -56044
- package/dist/steering.mjs +2 -0
- package/dist/testkit.mjs +13006 -0
- package/dist/wire.mjs +2 -0
- package/dist/workflow-kit.mjs +352 -0
- package/dist/workflows/feature.mjs +315 -0
- package/dist/workflows/fix.mjs +40 -0
- package/dist/workflows/review.mjs +25 -0
- package/dist/workflows.mjs +1953 -1724
- package/package.json +70 -8
- package/skills/authoring/SKILL.md +5 -4
- package/src/cli/Cli.test.ts +1329 -0
- package/src/cli/Cli.ts +1348 -0
- package/src/cli/Program.test.ts +2262 -0
- package/src/cli/Program.ts +1331 -0
- package/src/cli/ToolCommands.ts +43 -0
- package/src/cli/index.ts +14 -0
- package/src/edge/Edge.test.ts +1016 -0
- package/src/edge/Edge.ts +1225 -0
- package/src/edge/index.ts +16 -0
- package/src/emit/Emit.test.ts +425 -0
- package/src/emit/Emit.ts +139 -0
- package/src/emit/GitScript.test.ts +260 -0
- package/src/emit/GitScript.ts +131 -0
- package/src/emit/ModeContradiction.test.ts +120 -0
- package/src/emit/ModeContradiction.ts +66 -0
- package/src/emit/OutcomeScript.test.ts +238 -0
- package/src/emit/OutcomeScript.ts +73 -0
- package/src/emit/SteeringMode.test.ts +274 -0
- package/src/emit/SteeringMode.ts +256 -0
- package/src/emit/index.ts +40 -0
- package/src/flows/factory.ts +5 -1
- package/src/flows/runtime.ts +3 -0
- package/src/install/ConfigScaffold.ts +19 -0
- package/src/install/Install.test.ts +426 -0
- package/src/install/Install.ts +634 -0
- package/src/install/Version.ts +27 -0
- package/src/install/index.ts +3 -0
- package/src/platform/CommandRunner.ts +74 -0
- package/src/platform/Commentary.test.ts +126 -0
- package/src/platform/Commentary.ts +92 -0
- package/src/platform/Git.test.ts +50 -0
- package/src/platform/Git.ts +552 -0
- package/src/platform/Host.ts +42 -0
- package/src/platform/RetainedHistory.test.ts +65 -0
- package/src/platform/RetainedHistory.ts +34 -0
- package/src/platform/Workspace.test.ts +669 -0
- package/src/platform/Workspace.ts +631 -0
- package/src/platform/WorktreeState.test.ts +162 -0
- package/src/platform/WorktreeState.ts +50 -0
- package/src/platform/index.ts +22 -0
- package/src/replay/Access.test.ts +53 -0
- package/src/replay/Access.ts +46 -0
- package/src/replay/Format.test.ts +20 -0
- package/src/replay/Format.ts +14 -0
- package/src/replay/Replay.test.ts +887 -0
- package/src/replay/Replay.ts +669 -0
- package/src/replay/Trailers.test.ts +117 -0
- package/src/replay/Trailers.ts +213 -0
- package/src/replay/Tree.test.ts +25 -0
- package/src/replay/Tree.ts +45 -0
- package/src/replay/index.ts +11 -0
- package/src/steering/Blocks.fixture.ts +6 -0
- package/src/steering/Blocks.test.ts +167 -0
- package/src/steering/Blocks.ts +321 -0
- package/src/steering/CodeThreads.test.ts +127 -0
- package/src/steering/CodeThreads.ts +197 -0
- package/src/steering/Edits.test.ts +19 -0
- package/src/steering/Edits.ts +20 -0
- package/src/steering/Eol.test.ts +16 -0
- package/src/steering/Eol.ts +2 -0
- package/src/steering/FORMATS.md +58 -0
- package/src/steering/Footnotes.test.ts +977 -0
- package/src/steering/Footnotes.ts +661 -0
- package/src/steering/Inline.fixture.ts +7 -0
- package/src/steering/Inline.test.ts +227 -0
- package/src/steering/Inline.ts +195 -0
- package/src/steering/MarkdownTree.test.ts +247 -0
- package/src/steering/MarkdownTree.ts +260 -0
- package/src/steering/Outline.ts +19 -0
- package/src/steering/SteeringFormat.test.ts +91 -0
- package/src/steering/SteeringFormat.ts +267 -0
- package/src/steering/SteeringFormats.test.ts +163 -0
- package/src/steering/Threads.ts +163 -0
- package/src/steering/contract.test.ts +264 -0
- package/src/steering/fixtures/code-threads/cache.stripped.ts.txt +6 -0
- package/src/steering/fixtures/code-threads/cache.ts.md +15 -0
- package/src/steering/fixtures/free-form/notes.md +12 -0
- package/src/steering/fixtures/free-form/notes.read.json +42 -0
- package/src/steering/fixtures/qa/answered.md +23 -0
- package/src/steering/fixtures/qa/answered.read.json +119 -0
- package/src/steering/fixtures/qa/requirements.md +160 -0
- package/src/steering/fixtures/qa/requirements.read.json +475 -0
- package/src/steering/fixtures/review/review.md +76 -0
- package/src/steering/fixtures/review/review.read.json +237 -0
- package/src/steering/fixtures/review/ticked.md +23 -0
- package/src/steering/fixtures/review/ticked.read.json +99 -0
- package/src/steering/fixtures/review-notes/baseline.md +14 -0
- package/src/steering/fixtures/review-notes/noted.md +19 -0
- package/src/steering/freeform.test.ts +311 -0
- package/src/steering/freeform.ts +148 -0
- package/src/steering/index.test.ts +62 -0
- package/src/steering/index.ts +140 -0
- package/src/steering/qa.test.ts +2160 -0
- package/src/steering/qa.ts +1104 -0
- package/src/steering/review.test.ts +1829 -0
- package/src/steering/review.ts +1180 -0
- package/src/step/LandStep.ts +31 -0
- package/src/step/RepoSnapshot.ts +29 -0
- package/src/step/accessRefusal.test.ts +58 -0
- package/src/step/accessRefusal.ts +26 -0
- package/src/step/index.ts +5 -0
- package/src/step/planStart.test.ts +202 -0
- package/src/step/planStart.ts +167 -0
- package/src/step/planStep.test.ts +104 -0
- package/src/step/planStep.ts +89 -0
- package/src/step/snapshot.fixture.ts +28 -0
- package/src/testkit/EmittedScriptRecognizer.test.ts +639 -0
- package/src/testkit/EmittedScriptRecognizer.ts +642 -0
- package/src/testkit/FakeGitOperations.ts +95 -0
- package/src/testkit/GitTiers.test.ts +11 -0
- package/src/testkit/GitTiers.ts +1052 -0
- package/src/testkit/InMemRepo.ts +467 -0
- package/src/testkit/Layers.test.ts +58 -0
- package/src/testkit/Layers.ts +241 -0
- package/src/testkit/StrictGitOperations.ts +31 -0
- package/src/testkit/TestRepo.test.ts +119 -0
- package/src/testkit/TestRepo.ts +274 -0
- package/src/testkit/cliIo.ts +36 -0
- package/src/testkit/index.ts +9 -0
- package/src/testkit/steps.ts +177 -0
- package/src/testkit/testkit.feature +65 -0
- package/src/wire/BeatDocument.test.ts +419 -0
- package/src/wire/BeatDocument.ts +114 -0
- package/src/wire/BeatPlain.test.ts +113 -0
- package/src/wire/BeatPlain.ts +127 -0
- package/src/wire/BeatStatus.test.ts +59 -0
- package/src/wire/BeatStatus.ts +81 -0
- package/src/wire/Demand.test.ts +142 -0
- package/src/wire/Demand.ts +108 -0
- package/src/wire/ExitCodes.test.ts +21 -0
- package/src/wire/ExitCodes.ts +16 -0
- package/src/wire/Glob.ts +29 -0
- package/src/wire/Judge.ts +22 -0
- package/src/wire/Land.test.ts +120 -0
- package/src/wire/Land.ts +75 -0
- package/src/wire/Select.test.ts +148 -0
- package/src/wire/Select.ts +78 -0
- package/src/wire/Sessions.test.ts +59 -0
- package/src/wire/Sessions.ts +39 -0
- package/src/wire/WireSchema.test.ts +162 -0
- package/src/wire/constants.ts +22 -0
- package/src/wire/golden/1/beat-capture.json +34 -0
- package/src/wire/golden/1/beat-message.json +34 -0
- package/src/wire/golden/1/beat-prompt.json +44 -0
- package/src/wire/golden/1/beat-script.json +34 -0
- package/src/wire/golden/1/beat-stalled.json +34 -0
- package/src/wire/golden/1/judge.json +14 -0
- package/src/wire/golden/1/land-noop.json +10 -0
- package/src/wire/golden/1/land.json +10 -0
- package/src/wire/index.ts +12 -0
- package/src/wire/types.ts +46 -0
- package/src/workflow/Diagnostic.test.ts +93 -0
- package/src/workflow/Diagnostic.ts +54 -0
- package/src/workflow/SteeringFormats.test.ts +27 -0
- package/src/workflow/SteeringFormats.ts +6 -0
- package/src/workflow/Workflow.ts +77 -0
- package/src/workflow/compile.test.ts +53 -0
- package/src/workflow/compile.ts +56 -0
- package/src/workflow/discovery.ts +122 -0
- package/src/workflow/fingerprint.test.ts +52 -0
- package/src/workflow/fingerprint.ts +53 -0
- package/src/workflow/index.ts +33 -0
- package/src/workflow/load.test.ts +1017 -0
- package/src/workflow/load.ts +836 -0
- package/src/workflow/skills.test.ts +109 -0
- package/src/workflow/skills.ts +90 -0
- package/src/workflow/vars.ts +33 -0
- package/src/workflow-kit/health.test.ts +30 -0
- package/src/workflow-kit/health.ts +86 -0
- package/src/workflow-kit/index.ts +7 -0
- package/src/workflow-kit/quality.test.ts +17 -0
- package/src/workflow-kit/quality.ts +45 -0
- package/src/workflow-kit/review.ts +324 -0
- package/src/{workflows → workflow-kit}/scenarios.ts +21 -28
- package/src/workflows/feature/feature.test.ts +32 -0
- package/src/workflows/{bundled.ts → feature/feature.ts} +11 -63
- package/src/workflows/feature/index.ts +10 -0
- package/src/workflows/{packages.test.ts → feature/packages.test.ts} +2 -2
- package/src/workflows/{packages.ts → feature/packages.ts} +3 -3
- package/src/workflows/{planning.ts → feature/planning.ts} +2 -3
- package/src/workflows/{review.test.ts → feature/review.test.ts} +9 -44
- package/src/workflows/feature/review.ts +106 -0
- package/src/workflows/feature/steps.test.ts +82 -0
- package/src/workflows/feature/steps.ts +92 -0
- package/src/workflows/fix/fix.feature +80 -0
- package/src/workflows/fix/fix.test.ts +74 -0
- package/src/workflows/fix/fix.ts +52 -0
- package/src/workflows/fix/index.ts +6 -0
- package/src/workflows/index.ts +36 -2
- package/src/workflows/review/index.ts +6 -0
- package/src/workflows/review/review.test.ts +99 -0
- package/src/workflows/review/review.ts +33 -0
- package/src/workflows/{access.test.ts → shared/access.test.ts} +1 -1
- package/src/workflows/{access.ts → shared/access.ts} +3 -5
- package/src/workflows/shared/config.fixture.ts +12 -0
- package/src/workflows/{health.test.ts → shared/health.test.ts} +1 -1
- package/src/workflows/{health.ts → shared/health.ts} +38 -55
- package/src/workflows/{options.test.ts → shared/options.test.ts} +2 -2
- package/src/workflows/{options.ts → shared/options.ts} +14 -2
- package/src/workflows/shared/reserved.ts +11 -0
- package/src/workflows/{scenarios.test.ts → shared/scenarios.test.ts} +1 -1
- package/src/workflows/shared/scenarios.ts +35 -0
- package/src/workflows/{skills.test.ts → shared/skills.test.ts} +3 -1
- package/src/workflows/{skills.ts → shared/skills.ts} +7 -6
- package/src/workflows/{steps.test.ts → shared/steps.test.ts} +1 -66
- package/src/workflows/{steps.ts → shared/steps.ts} +8 -98
- package/src/workflows/shared/tail.test.ts +205 -0
- package/src/workflows/shared/tail.ts +135 -0
- package/src/workflows/{text.fixture.ts → shared/text.fixture.ts} +2 -2
- package/src/workflows/{text.ts → shared/text.ts} +48 -7
- package/src/workflows/{threads.test.ts → shared/threads.test.ts} +1 -1
- package/src/workflows/shared/workflows.steps.ts +7 -0
- package/src/workflows/review.ts +0 -362
- /package/src/workflows/{prose.ts → shared/prose.ts} +0 -0
- /package/src/workflows/{text.test.ts → shared/text.test.ts} +0 -0
- /package/src/workflows/{vars.ts → shared/vars.ts} +0 -0
package/dist/wire.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { C as EXIT_SIGTERM, D as WIRE_SCHEMA, E as UNATTRIBUTED_MODEL, O as renderFormat, S as EXIT_SIGINT, T as ACCESS_REFUSAL, _ as demandOf, a as landFields, b as EXIT_OK, c as renderLandJson, d as renderBeatPlain, f as beatDocument, g as beatKindOf, h as BEAT_KINDS, i as judgeJson, l as renderLandPlain, m as statusOf, n as resolveSession, o as landProseText, p as renderBeatJson, r as selectPath, s as noopText, t as globMatches, u as DEMAND_BRIEFING, v as stallDiagnosis, w as EXIT_USAGE_ERROR, x as EXIT_RUNTIME_ERROR, y as EXIT_CODES } from "./chunks/wire-DA7q2ikx.mjs";
|
|
2
|
+
export { ACCESS_REFUSAL, BEAT_KINDS, DEMAND_BRIEFING, EXIT_CODES, EXIT_OK, EXIT_RUNTIME_ERROR, EXIT_SIGINT, EXIT_SIGTERM, EXIT_USAGE_ERROR, UNATTRIBUTED_MODEL, WIRE_SCHEMA, beatDocument, beatKindOf, demandOf, globMatches, judgeJson, landFields, landProseText, noopText, renderBeatJson, renderBeatPlain, renderFormat, renderLandJson, renderLandPlain, resolveSession, selectPath, stallDiagnosis, statusOf };
|
|
@@ -0,0 +1,352 @@
|
|
|
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-D-PvRC5W.mjs";
|
|
3
|
+
//#region src/workflow-kit/health.ts
|
|
4
|
+
/** Hand a red suite that repeated attempts could not fix to a person, until `limit` rounds are spent. */
|
|
5
|
+
const escalation = async (count, steps) => {
|
|
6
|
+
if (count.rounds >= steps.limit) return steps.exhausted();
|
|
7
|
+
count.rounds++;
|
|
8
|
+
await steps.describe();
|
|
9
|
+
await steps.stop();
|
|
10
|
+
};
|
|
11
|
+
/** Run `check` again while `pending()` holds — a run then does not count — and resolve the first run that does. */
|
|
12
|
+
const untilSettled = async (check, pending) => {
|
|
13
|
+
for (;;) {
|
|
14
|
+
const green = await check();
|
|
15
|
+
if (!pending()) return green;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const STAMP = /\n<!-- gtd check [0-9a-f]+ -->\n?$/;
|
|
19
|
+
const stripStamp = (text) => text.replace(STAMP, "\n");
|
|
20
|
+
/**
|
|
21
|
+
* Loop until the suite is green: check, and on red run `fix` — escalating
|
|
22
|
+
* first once `fixCap` fixes are spent, or `sameFailure` calls the failure
|
|
23
|
+
* identical to the previous round's.
|
|
24
|
+
*/
|
|
25
|
+
const healthy = async (fix, health) => {
|
|
26
|
+
let fixes = health.fixesSoFar ?? 0;
|
|
27
|
+
let previous;
|
|
28
|
+
for (;;) {
|
|
29
|
+
if (await health.check()) {
|
|
30
|
+
health.escalations.rounds = 0;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const current = stripStamp(read(health.report) ?? "");
|
|
34
|
+
const stuck = previous !== void 0 && await health.sameFailure(previous, current);
|
|
35
|
+
previous = current;
|
|
36
|
+
if (stuck || fixes >= health.fixCap) {
|
|
37
|
+
await escalation(health.escalations, health.escalation);
|
|
38
|
+
fixes = 0;
|
|
39
|
+
}
|
|
40
|
+
fixes++;
|
|
41
|
+
await fix();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
/** Hold the process in scope `name` until `passes` resolves `true`, resting at `blocked` between tries. */
|
|
45
|
+
const gate = (name, passes, blocked) => scope(name, async () => {
|
|
46
|
+
while (!await passes()) await blocked();
|
|
47
|
+
});
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/workflow-kit/quality.ts
|
|
50
|
+
/** Split a comma-separated lens list: trimmed, blanks dropped. */
|
|
51
|
+
const lensesOf = (value) => value.split(",").map((lens) => lens.trim()).filter((lens) => lens.length > 0);
|
|
52
|
+
/** One `<scope>.<lens>` entry per lens — the per-lens part of a skills or access export. */
|
|
53
|
+
const lensScopes = (scope, lenses, entry) => Object.fromEntries(lenses.map((lens) => [`${scope}.${lens}`, entry(lens)]));
|
|
54
|
+
/** One `review` turn per lens, each appending its findings to `findings`; resolves `"findings"` when that file has any. */
|
|
55
|
+
const qualityLap = async (lenses, review, findings) => {
|
|
56
|
+
for (const lens of lenses) await review(lens);
|
|
57
|
+
return (read(findings) ?? "").length > 0 ? "findings" : "clean";
|
|
58
|
+
};
|
|
59
|
+
/** Resolves `true` once `findings` is gone, `false` when `cap` turns escalated instead. */
|
|
60
|
+
const fixFindings = async (f) => {
|
|
61
|
+
for (let turns = 0; turns < f.cap; turns++) {
|
|
62
|
+
await f.fix();
|
|
63
|
+
if (read(f.findings) === void 0) return true;
|
|
64
|
+
}
|
|
65
|
+
await f.escalate();
|
|
66
|
+
return false;
|
|
67
|
+
};
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/workflow-kit/scenarios.ts
|
|
70
|
+
const DOC_STRING = /^(\s*)("""|```)/;
|
|
71
|
+
/**
|
|
72
|
+
* Gherkin wording only: lines trimmed; blank, `#` comment and `@` tag lines
|
|
73
|
+
* dropped — except inside a doc string, whose content is test input: kept
|
|
74
|
+
* whole, indented relative to its opening delimiter (as Gherkin reads it).
|
|
75
|
+
*/
|
|
76
|
+
const scenarioText = (source) => {
|
|
77
|
+
const out = [];
|
|
78
|
+
let doc;
|
|
79
|
+
for (const line of source.split("\n")) {
|
|
80
|
+
const delimiter = DOC_STRING.exec(line);
|
|
81
|
+
if (doc === void 0 && delimiter !== null) {
|
|
82
|
+
doc = {
|
|
83
|
+
indent: delimiter[1].length,
|
|
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;
|
|
111
|
+
}
|
|
112
|
+
return Object.keys(texts).length === 0 ? previous : {
|
|
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;
|
|
125
|
+
};
|
|
126
|
+
/** Changed lines in file order, `- ` / `+ `-prefixed, so a moved or repeated step still shows. */
|
|
127
|
+
const lineDiff = (a, b) => {
|
|
128
|
+
const table = lcsTable(a, b);
|
|
129
|
+
const out = [];
|
|
130
|
+
let i = 0;
|
|
131
|
+
let j = 0;
|
|
132
|
+
while (i < a.length && j < b.length) if (a[i] === b[j]) {
|
|
133
|
+
i++;
|
|
134
|
+
j++;
|
|
135
|
+
} else if (table[i + 1][j] >= table[i][j + 1]) out.push(`- ${a[i++]}`);
|
|
136
|
+
else out.push(`+ ${b[j++]}`);
|
|
137
|
+
return [
|
|
138
|
+
...out,
|
|
139
|
+
...a.slice(i).map((l) => `- ${l}`),
|
|
140
|
+
...b.slice(j).map((l) => `+ ${l}`)
|
|
141
|
+
];
|
|
142
|
+
};
|
|
143
|
+
const wordingDrift = (frozen, path) => ({
|
|
144
|
+
path,
|
|
145
|
+
lines: lineDiff(lines(frozen.texts[path]), lines(textOf(path)))
|
|
146
|
+
});
|
|
147
|
+
/**
|
|
148
|
+
* After a turn: rest at `stop` while wording drifted from `frozen`. Whatever
|
|
149
|
+
* still differs after it is accepted and moves the snapshot; what was
|
|
150
|
+
* restored stays frozen as it was.
|
|
151
|
+
*/
|
|
152
|
+
const holdWording = async (frozen, stop) => {
|
|
153
|
+
if (frozen === void 0) return;
|
|
154
|
+
const drifted = driftedScenarios(frozen);
|
|
155
|
+
if (drifted.length === 0) return;
|
|
156
|
+
await stop(frozen.at, drifted.map((path) => wordingDrift(frozen, path)));
|
|
157
|
+
const accepted = driftedScenarios(frozen);
|
|
158
|
+
if (accepted.length === 0) return;
|
|
159
|
+
for (const path of accepted) {
|
|
160
|
+
const text = textOf(path);
|
|
161
|
+
if (text === void 0) delete frozen.texts[path];
|
|
162
|
+
else frozen.texts[path] = text;
|
|
163
|
+
}
|
|
164
|
+
frozen.at = head();
|
|
165
|
+
};
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/workflow-kit/review.ts
|
|
168
|
+
const collect = async (p, capture) => {
|
|
169
|
+
await p.collecting(capture);
|
|
170
|
+
if (read(p.feedback) !== void 0) return "feedback";
|
|
171
|
+
if (changes().length === 0) return "signoff";
|
|
172
|
+
return refuse(p.refusals.unmatched);
|
|
173
|
+
};
|
|
174
|
+
/** A review with ticks, footnote markers and thread definitions removed: what is left is line notes. */
|
|
175
|
+
const notesOf = (text) => text.replace(/^\[\^[^\]]+\]:[^\n]*(\n(?: {2,}|\t)[^\n]*|\n(?=\s*\n(?: {2,}|\t)))*/gm, "").replace(/\[\^[^\]]+\]/g, "").replace(/\[x\]/gi, "[ ]").replace(/\s+/g, " ").trim();
|
|
176
|
+
/**
|
|
177
|
+
* The human gate and its reply rounds: a question gets an answer inside
|
|
178
|
+
* the review file and rests at the gate again, without a lap. Resolves the
|
|
179
|
+
* commit of the last collecting turn, `"missing"` when the round left no
|
|
180
|
+
* review, or `undefined` when no such turn ran.
|
|
181
|
+
*/
|
|
182
|
+
const converse = async (p, base, collectedBefore) => {
|
|
183
|
+
let collectedAt = collectedBefore;
|
|
184
|
+
for (;;) {
|
|
185
|
+
await p.awaitReview(base);
|
|
186
|
+
if (changes(p.file).some((c) => c.status === "deleted")) refuse(p.refusals.deleted);
|
|
187
|
+
if (read(p.file) === void 0) {
|
|
188
|
+
await p.reviewMissing(head());
|
|
189
|
+
return "missing";
|
|
190
|
+
}
|
|
191
|
+
requireThreadsClosed(p.file);
|
|
192
|
+
if (!hasThreadFor("agent", p.file)) return collectedAt;
|
|
193
|
+
await p.collecting(p.captures.edits(head()));
|
|
194
|
+
collectedAt = head();
|
|
195
|
+
requireReplies(p.file);
|
|
196
|
+
if (!hasThreadFor("human", p.file)) return collectedAt;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
/** Judge every note, then run each verdict's route: answers, then nit fixes, then the edits' lap. */
|
|
200
|
+
const routeNotes = async (p, notes, r) => {
|
|
201
|
+
const verdicts = await p.triage(notes);
|
|
202
|
+
const of = (v) => notes.filter((n) => (verdicts.get(n.id) ?? "edit") === v);
|
|
203
|
+
const [edits, questions, nits] = [
|
|
204
|
+
of("edit"),
|
|
205
|
+
of("question"),
|
|
206
|
+
of("nit")
|
|
207
|
+
];
|
|
208
|
+
if (edits.length + questions.length + nits.length === 0) return r.unfolded();
|
|
209
|
+
let answeredAt;
|
|
210
|
+
if (questions.length > 0) {
|
|
211
|
+
await p.answerQuestions(questions);
|
|
212
|
+
answeredAt = head();
|
|
213
|
+
}
|
|
214
|
+
if (nits.length > 0) {
|
|
215
|
+
await p.fixNits(nits);
|
|
216
|
+
await p.keepGreen();
|
|
217
|
+
}
|
|
218
|
+
if (edits.length > 0) {
|
|
219
|
+
await r.close();
|
|
220
|
+
return r.outcome(await collect(p, p.captures.editNotes(r.round, edits, answeredAt)));
|
|
221
|
+
}
|
|
222
|
+
if (nits.length > 0) {
|
|
223
|
+
await r.close();
|
|
224
|
+
return {
|
|
225
|
+
next: "rereview",
|
|
226
|
+
carry: answeredAt
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
next: "await",
|
|
231
|
+
reviewed: answeredAt
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
/** A code edit, not a thread-only change: lines that only add, answer or remove code threads don't count. */
|
|
235
|
+
const isCodeEdit = (p, c) => p.isCode(c.path) && stripCodeThreads(c.path, c.before ?? "") !== stripCodeThreads(c.path, c.after ?? "");
|
|
236
|
+
/** Whether anything but thread edits and ticks landed in the review file since `commit`. */
|
|
237
|
+
const notedSince = (p, commit) => changesSince(commit).some((c) => c.path === p.file && notesOf(c.before ?? "") !== notesOf(read(p.file) ?? ""));
|
|
238
|
+
/**
|
|
239
|
+
* What the in-loop collecting turn left: `folded` when it wrote `feedback`
|
|
240
|
+
* (those folds need their lap whatever is judged later), `settled` when only
|
|
241
|
+
* thread edits and ticks landed after it.
|
|
242
|
+
*/
|
|
243
|
+
const afterCollect = (p, collectedAt) => collectedAt === "" ? {
|
|
244
|
+
folded: false,
|
|
245
|
+
settled: false
|
|
246
|
+
} : settledAfter(p, collectedAt);
|
|
247
|
+
const settledAfter = (p, collectedAt) => ({
|
|
248
|
+
folded: collectedAt !== void 0 && read(p.feedback) !== void 0,
|
|
249
|
+
settled: collectedAt !== void 0 && changesSince(collectedAt).every((c) => !isCodeEdit(p, c)) && !notedSince(p, collectedAt)
|
|
250
|
+
});
|
|
251
|
+
const finish = async (p, reviewed, collectedAt) => {
|
|
252
|
+
const round = head();
|
|
253
|
+
const since = changesSince(reviewed);
|
|
254
|
+
const edited = since.filter((c) => isCodeEdit(p, c));
|
|
255
|
+
const baselineText = since.get(p.file)?.before ?? "";
|
|
256
|
+
const record = read(p.file) ?? "";
|
|
257
|
+
const { folded, settled } = afterCollect(p, collectedAt);
|
|
258
|
+
const noted = since.some((c) => c.path === p.file);
|
|
259
|
+
const close = () => p.close(round);
|
|
260
|
+
const outcome = (verdict) => verdict === "signoff" ? { verdict } : {
|
|
261
|
+
verdict,
|
|
262
|
+
base: round,
|
|
263
|
+
restoreFrom: reviewed,
|
|
264
|
+
edited
|
|
265
|
+
};
|
|
266
|
+
const unfolded = async () => {
|
|
267
|
+
await close();
|
|
268
|
+
return outcome(folded ? "feedback" : "signoff");
|
|
269
|
+
};
|
|
270
|
+
const collectWith = async (capture) => {
|
|
271
|
+
await close();
|
|
272
|
+
return outcome(await collect(p, capture));
|
|
273
|
+
};
|
|
274
|
+
if (settled || edited.length === 0 && !noted) return unfolded();
|
|
275
|
+
if (edited.length > 0) return collectWith(p.captures.edits(round));
|
|
276
|
+
const notes = reviewNotes(baselineText, record);
|
|
277
|
+
if (notes.length === 0) return collectWith(p.captures.notes(round));
|
|
278
|
+
return routeNotes(p, notes, {
|
|
279
|
+
round,
|
|
280
|
+
close,
|
|
281
|
+
outcome,
|
|
282
|
+
unfolded
|
|
283
|
+
});
|
|
284
|
+
};
|
|
285
|
+
/** Write the review; if it marks risks, fix them, keep green, and write it again — once, so the re-review's own risks reach the human unfixed. */
|
|
286
|
+
const reviewOnce = async (p, base, carry) => {
|
|
287
|
+
await p.reviewing(base, carry);
|
|
288
|
+
const risks = reviewRisks(read(p.file) ?? "");
|
|
289
|
+
if (risks.length === 0) return;
|
|
290
|
+
await p.fixRisks(risks);
|
|
291
|
+
await p.keepGreen();
|
|
292
|
+
await p.reviewing(base, carry);
|
|
293
|
+
};
|
|
294
|
+
/**
|
|
295
|
+
* A reviewer writes the review over everything since `base`, a human
|
|
296
|
+
* reviews and signs off or comments, and each note is judged: edits go to
|
|
297
|
+
* another lap, questions are answered in place, nits fixed in one batch,
|
|
298
|
+
* praise dropped.
|
|
299
|
+
*/
|
|
300
|
+
const review = async (base, p) => {
|
|
301
|
+
let carry;
|
|
302
|
+
for (;;) {
|
|
303
|
+
await reviewOnce(p, base, carry);
|
|
304
|
+
carry = void 0;
|
|
305
|
+
let reviewed = head();
|
|
306
|
+
let collectedAt;
|
|
307
|
+
for (;;) {
|
|
308
|
+
collectedAt = await converse(p, base, collectedAt);
|
|
309
|
+
if (collectedAt === "missing") break;
|
|
310
|
+
const result = await finish(p, reviewed, collectedAt);
|
|
311
|
+
if (!("next" in result)) return result;
|
|
312
|
+
if (result.next === "rereview") {
|
|
313
|
+
carry = result.carry;
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
reviewed = result.reviewed;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
/**
|
|
321
|
+
* A reviewer writes the review over everything since `base` and a human
|
|
322
|
+
* reviews it. No note signs off and closes; any note ends the round with the
|
|
323
|
+
* review left in place, for another workflow to read its notes — nothing is
|
|
324
|
+
* judged or re-planned here.
|
|
325
|
+
*/
|
|
326
|
+
const reviewToHandOff = async (base, p) => {
|
|
327
|
+
for (;;) {
|
|
328
|
+
await reviewOnce(p, base, void 0);
|
|
329
|
+
const reviewed = head();
|
|
330
|
+
await p.awaitReview(base);
|
|
331
|
+
if (changes(p.file).some((c) => c.status === "deleted")) refuse(p.deleted);
|
|
332
|
+
const record = read(p.file);
|
|
333
|
+
if (record === void 0) {
|
|
334
|
+
await p.reviewMissing(head());
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
requireThreadsClosed(p.file);
|
|
338
|
+
if (hasThreadFor("agent", p.file)) refuse(p.threadsPending);
|
|
339
|
+
const since = changesSince(reviewed);
|
|
340
|
+
const before = since.get(p.file)?.before ?? record;
|
|
341
|
+
if (notesOf(before) === notesOf(record)) {
|
|
342
|
+
if (since.paths.some((f) => !f.startsWith(".gtd/"))) await p.keepGreen();
|
|
343
|
+
await p.close(head());
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (reviewNotes(before, record).length === 0) refuse(p.unparsed);
|
|
347
|
+
await p.handOff(reviewed);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
//#endregion
|
|
352
|
+
export { driftedScenarios, escalation, fixFindings, freezeScenarios, gate, healthy, holdWording, lensScopes, lensesOf, qualityLap, review, reviewToHandOff, scenarioText, untilSettled, wordingDrift };
|
|
@@ -0,0 +1,315 @@
|
|
|
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 { freezeScenarios, review as review$1 } from "../workflow-kit.mjs";
|
|
3
|
+
import { $ as unwindFailure, A as architectureAuthorPrompt, B as designTriagePrompt, C as fixCheck, D as reviewing, E as reviewMissing, F as buildReviewFixNitsPrompt, G as packagesItemFixSuitePrompt, I as buildReviewTriageMessage, K as reviewEditNotesCapture, L as builderSystem, M as architectureGateAnswerMessage, N as buildReviewAnswerQuestionsPrompt, O as agentWithSkills, P as buildReviewCollectingPrompt, Q as unwindFailedMessage, R as designGateAnswerMessage, S as coder, T as planner, U as idleMessage, W as packagesItemBuildingPrompt, X as reviewerSystem, Y as reviewNotesCapture, Z as startGateBlockedMessage, _ as ARCHITECTURE, b as REVIEW, c as refusePendingHandOff, d as shipped, f as defaults, g as healthy, h as gate, i as skills, j as architectureDecomposePrompt, k as architectSystem, l as tail, n as summary, o as closeReview, p as envDefaults, q as reviewEditsCapture, r as access, t as steering, u as guarded, v as FEEDBACK, w as fixRisks, x as awaitReview, y as REQUIREMENTS, z as designSystem } from "../chunks/reserved-ejn-LVYO.mjs";
|
|
4
|
+
//#region src/workflows/feature/steps.ts
|
|
5
|
+
/** Turn the sketch since `base` into `.gtd/REQUIREMENTS.md`. */
|
|
6
|
+
const triage = (base) => agentWithSkills("triage", designTriagePrompt(base), {
|
|
7
|
+
label: "Triaging the change",
|
|
8
|
+
file: REQUIREMENTS,
|
|
9
|
+
mode: "qa",
|
|
10
|
+
model: planner(),
|
|
11
|
+
system: designSystem()
|
|
12
|
+
});
|
|
13
|
+
const author = () => agentWithSkills("author", architectureAuthorPrompt(), {
|
|
14
|
+
label: "Refining the technical plan",
|
|
15
|
+
file: ARCHITECTURE,
|
|
16
|
+
mode: "qa",
|
|
17
|
+
model: planner(),
|
|
18
|
+
system: architectSystem()
|
|
19
|
+
});
|
|
20
|
+
const decompose = () => agentWithSkills("decompose.decomposing", architectureDecomposePrompt(), {
|
|
21
|
+
label: "Decomposing into packages",
|
|
22
|
+
model: planner(),
|
|
23
|
+
system: architectSystem()
|
|
24
|
+
});
|
|
25
|
+
const answerProductQuestions = () => human("gate.answer", {
|
|
26
|
+
message: designGateAnswerMessage(),
|
|
27
|
+
label: "Awaiting your product answers",
|
|
28
|
+
file: REQUIREMENTS,
|
|
29
|
+
mode: "qa",
|
|
30
|
+
acceptClean: true
|
|
31
|
+
});
|
|
32
|
+
const answerTechnicalQuestions = () => human("gate.answer", {
|
|
33
|
+
message: architectureGateAnswerMessage(),
|
|
34
|
+
label: "Awaiting your technical answers",
|
|
35
|
+
file: ARCHITECTURE,
|
|
36
|
+
mode: "qa",
|
|
37
|
+
acceptClean: true
|
|
38
|
+
});
|
|
39
|
+
const build = (pkg) => agentWithSkills("building", packagesItemBuildingPrompt(pkg), {
|
|
40
|
+
label: "Building",
|
|
41
|
+
model: coder(),
|
|
42
|
+
system: builderSystem()
|
|
43
|
+
});
|
|
44
|
+
const fixSuite = () => agentWithSkills("fix.suite.fixing", packagesItemFixSuitePrompt(), {
|
|
45
|
+
label: "Fixing the check",
|
|
46
|
+
file: FEEDBACK,
|
|
47
|
+
model: coder(),
|
|
48
|
+
system: builderSystem()
|
|
49
|
+
});
|
|
50
|
+
/** Answer every `question` note inline in `.gtd/REVIEW.md`. */
|
|
51
|
+
const answerReviewQuestions = (notes) => agentWithSkills("review.answer-review-questions", buildReviewAnswerQuestionsPrompt(notes), {
|
|
52
|
+
label: "Answering your questions",
|
|
53
|
+
file: REVIEW,
|
|
54
|
+
mode: "review",
|
|
55
|
+
model: planner(),
|
|
56
|
+
system: reviewerSystem()
|
|
57
|
+
});
|
|
58
|
+
/** Fix every `nit` note in one turn. */
|
|
59
|
+
const fixNits = (notes) => agentWithSkills("review.fix.nits.fixing", buildReviewFixNitsPrompt(notes), {
|
|
60
|
+
label: "Fixing your nits",
|
|
61
|
+
file: REVIEW,
|
|
62
|
+
model: planner(),
|
|
63
|
+
system: reviewerSystem()
|
|
64
|
+
});
|
|
65
|
+
/** Turn the review round `capture` describes into `.gtd/REQUIREMENTS.md`, or change nothing. */
|
|
66
|
+
const collecting = (capture) => agentWithSkills("review.collecting", buildReviewCollectingPrompt(capture), {
|
|
67
|
+
label: "Collecting your feedback",
|
|
68
|
+
file: REQUIREMENTS,
|
|
69
|
+
mode: "qa",
|
|
70
|
+
model: planner(),
|
|
71
|
+
system: reviewerSystem(),
|
|
72
|
+
allowEmpty: true
|
|
73
|
+
});
|
|
74
|
+
//#endregion
|
|
75
|
+
//#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
|
+
const TEST_LINE = /^\s*-\s+(unit|e2e):\s+`([^`]+)`/;
|
|
79
|
+
/** The `## Tests` entries of a package file; malformed lines are ignored. */
|
|
80
|
+
const declaredTests = (packageText) => {
|
|
81
|
+
const found = [];
|
|
82
|
+
let inTests = false;
|
|
83
|
+
for (const line of packageText.split("\n")) if (/^##\s/.test(line)) inTests = /^##\s+Tests\s*$/.test(line);
|
|
84
|
+
else if (inTests) {
|
|
85
|
+
const match = TEST_LINE.exec(line);
|
|
86
|
+
if (match) found.push({
|
|
87
|
+
level: match[1],
|
|
88
|
+
path: match[2]
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return found;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Refuse a build or fix turn unless every test `pkg` declares exists and is in the
|
|
95
|
+
* diff since `since`. The package text is read as of `since`, so a builder
|
|
96
|
+
* editing its own `## Tests` cannot dodge the guard; with `.gtd/SATISFIED.md`
|
|
97
|
+
* written, existing in the tree suffices.
|
|
98
|
+
*/
|
|
99
|
+
const requireDeclaredTests = (pkg, since) => {
|
|
100
|
+
const range = changesSince(since);
|
|
101
|
+
const text = range.get(pkg)?.before ?? read(pkg) ?? "";
|
|
102
|
+
const satisfied = read(".gtd/SATISFIED.md") !== void 0;
|
|
103
|
+
const missing = declaredTests(text).map((test) => test.path).filter((path) => satisfied ? read(path) === void 0 : range.get(path) === void 0 || range.get(path)?.status === "deleted" || read(path) === void 0);
|
|
104
|
+
if (missing.length === 0) return;
|
|
105
|
+
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
|
+
};
|
|
107
|
+
/** Build `pkg`, check its declared tests, keep the fast suite green, and close it out, which removes it. */
|
|
108
|
+
const packageItem = async (pkg, frozen) => {
|
|
109
|
+
const from = head();
|
|
110
|
+
const declared = (turn) => async () => {
|
|
111
|
+
await turn();
|
|
112
|
+
requireDeclaredTests(pkg, from);
|
|
113
|
+
};
|
|
114
|
+
await guarded(frozen, declared(() => build(pkg)))();
|
|
115
|
+
await healthy(guarded(frozen, declared(fixSuite)), { suite: "fast" });
|
|
116
|
+
await run("closing", removeScript([pkg, ".gtd/SATISFIED.md"]), { label: "Closing out the package" });
|
|
117
|
+
return {
|
|
118
|
+
pkg,
|
|
119
|
+
from,
|
|
120
|
+
to: head()
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
/** The first queued package — the one a package step works on. */
|
|
124
|
+
const nextPackage = () => [...glob(".gtd/packages/*.md")].sort()[0];
|
|
125
|
+
const outsideGtd = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
126
|
+
/** Build every package file under `.gtd/packages/`, in name order. */
|
|
127
|
+
const packages = (carried) => scope("packages", async () => {
|
|
128
|
+
let frozen = carried;
|
|
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
|
+
};
|
|
148
|
+
});
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region src/workflows/feature/planning.ts
|
|
151
|
+
/**
|
|
152
|
+
* Always stop for the human. Resolves `true` when the round changed anything
|
|
153
|
+
* (the author revises), `false` on a clean re-run. Open threads refuse every
|
|
154
|
+
* landing; unticked questions only refuse a round with no thread for the
|
|
155
|
+
* agent to answer.
|
|
156
|
+
*/
|
|
157
|
+
const noteGate = async (file, answer) => {
|
|
158
|
+
await answer();
|
|
159
|
+
requireThreadsClosed(file);
|
|
160
|
+
if (changes().length === 0) return false;
|
|
161
|
+
if (!hasThreadFor("agent", file)) requireAnswers(file);
|
|
162
|
+
return true;
|
|
163
|
+
};
|
|
164
|
+
/** Triage the sketch since `base` into requirements, until no product question is open. */
|
|
165
|
+
const design = (base) => scope("design", async () => {
|
|
166
|
+
do {
|
|
167
|
+
await triage(base);
|
|
168
|
+
requireProgress(REQUIREMENTS);
|
|
169
|
+
requireReplies(REQUIREMENTS);
|
|
170
|
+
} while (await noteGate(REQUIREMENTS, answerProductQuestions));
|
|
171
|
+
});
|
|
172
|
+
/** Work out the technical plan, then decompose it into package files. */
|
|
173
|
+
const architecture = () => scope("architecture", async () => {
|
|
174
|
+
do {
|
|
175
|
+
await author();
|
|
176
|
+
requireReplies(ARCHITECTURE);
|
|
177
|
+
} while (await noteGate(ARCHITECTURE, answerTechnicalQuestions));
|
|
178
|
+
await decompose();
|
|
179
|
+
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 build tail's full run needs it; it is removed only once that run is green");
|
|
181
|
+
});
|
|
182
|
+
//#endregion
|
|
183
|
+
//#region src/workflows/feature/review.ts
|
|
184
|
+
const VERDICTS = [
|
|
185
|
+
"edit",
|
|
186
|
+
"question",
|
|
187
|
+
"nit",
|
|
188
|
+
"praise"
|
|
189
|
+
];
|
|
190
|
+
/** One four-way choice per note the human added to `.gtd/REVIEW.md`. */
|
|
191
|
+
const verdictQuestion = (note) => ({
|
|
192
|
+
id: note.id,
|
|
193
|
+
primitive: "choice",
|
|
194
|
+
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
|
+
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
|
+
});
|
|
197
|
+
/**
|
|
198
|
+
* One judge rest, one verdict per note. Dismissing a note is the risky
|
|
199
|
+
* direction, so only a confident non-`edit` verdict on uncut evidence counts;
|
|
200
|
+
* a cut, unanswered or below-floor note is an `edit`, and a blank floor makes
|
|
201
|
+
* every note one.
|
|
202
|
+
*/
|
|
203
|
+
const triage$1 = async (notes) => {
|
|
204
|
+
const { answers, truncated } = await judge("review.triage", {
|
|
205
|
+
questions: notes.map(verdictQuestion),
|
|
206
|
+
evidence: Object.fromEntries(notes.map((n) => [n.id, `Anchor: ${n.anchor}\nReviewer's text: ${n.before}\nHuman's text: ${n.text}`])),
|
|
207
|
+
message: buildReviewTriageMessage(),
|
|
208
|
+
label: "Judging each note"
|
|
209
|
+
});
|
|
210
|
+
const minP = numeric(vars.reviewNoteActionable, Infinity);
|
|
211
|
+
return new Map(notes.map((n) => {
|
|
212
|
+
const verdict = VERDICTS.find((v) => v !== "edit" && answered(answers[n.id], v, minP));
|
|
213
|
+
return [n.id, truncated.includes(n.id) || verdict === void 0 ? "edit" : verdict];
|
|
214
|
+
}));
|
|
215
|
+
};
|
|
216
|
+
const isCode = (path) => path !== ".gtd" && !path.startsWith(".gtd/");
|
|
217
|
+
/**
|
|
218
|
+
* A reviewer writes `.gtd/REVIEW.md` over everything since `base`, a human
|
|
219
|
+
* reviews and signs off or comments, and each note is judged: edits go to
|
|
220
|
+
* another planning lap, questions are answered in place, nits fixed in one
|
|
221
|
+
* batch, praise dropped.
|
|
222
|
+
*/
|
|
223
|
+
const review = (base, escalations = { rounds: 0 }, frozen) => review$1(base, {
|
|
224
|
+
file: REVIEW,
|
|
225
|
+
feedback: REQUIREMENTS,
|
|
226
|
+
isCode,
|
|
227
|
+
reviewing,
|
|
228
|
+
fixRisks: (risks) => guarded(frozen, () => fixRisks(risks), "review")(),
|
|
229
|
+
keepGreen: () => healthy(guarded(frozen, fixCheck, "review"), { escalations }),
|
|
230
|
+
awaitReview,
|
|
231
|
+
reviewMissing,
|
|
232
|
+
collecting,
|
|
233
|
+
triage: triage$1,
|
|
234
|
+
answerQuestions: answerReviewQuestions,
|
|
235
|
+
fixNits: (notes) => guarded(frozen, () => fixNits(notes), "review")(),
|
|
236
|
+
close: closeReview,
|
|
237
|
+
captures: {
|
|
238
|
+
edits: reviewEditsCapture,
|
|
239
|
+
notes: reviewNotesCapture,
|
|
240
|
+
editNotes: reviewEditNotesCapture
|
|
241
|
+
},
|
|
242
|
+
refusals: {
|
|
243
|
+
deleted: "gtd land: review-doc: .gtd/REVIEW.md was deleted — restore it, or leave a note (or edit code) to request changes.",
|
|
244
|
+
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"
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
/** The build tail: fix (when entered red), keep green, the quality lap, then human review since `base`. */
|
|
248
|
+
const buildTail = (fixFirst, base, built) => tail(fixFirst, (escalations, frozen) => review(base, escalations, frozen), built);
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/workflows/feature/feature.ts
|
|
251
|
+
/** Revert the sketch that started the process out of the working tree; its intent survives in history. */
|
|
252
|
+
const unwind = () => {
|
|
253
|
+
const commit = head();
|
|
254
|
+
return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
|
|
255
|
+
};
|
|
256
|
+
/**
|
|
257
|
+
* Undo the human's review-round code edits. A path changed since the human
|
|
258
|
+
* left it is not overwritten; requireRevert names it instead.
|
|
259
|
+
*/
|
|
260
|
+
const reUnwind = async (feedback) => {
|
|
261
|
+
const { base, restoreFrom, edited } = feedback;
|
|
262
|
+
await run("re-unwind", restoreScript(base, {
|
|
263
|
+
restore: edited.filter((c) => c.status !== "added").map((c) => c.path),
|
|
264
|
+
remove: edited.filter((c) => c.status === "added").map((c) => c.path)
|
|
265
|
+
}, restoreFrom), {
|
|
266
|
+
label: "Re-unwinding your review edit",
|
|
267
|
+
file: REVIEW,
|
|
268
|
+
base
|
|
269
|
+
});
|
|
270
|
+
requireRevert(edited, base);
|
|
271
|
+
};
|
|
272
|
+
/** Plan, build and review until a review round signs off; feedback re-plans from scratch. */
|
|
273
|
+
const planAndBuild = async (firstBase) => {
|
|
274
|
+
let base = firstBase;
|
|
275
|
+
let carried;
|
|
276
|
+
for (;;) {
|
|
277
|
+
await design(base);
|
|
278
|
+
await architecture();
|
|
279
|
+
const built = await packages(carried);
|
|
280
|
+
carried = built.frozen;
|
|
281
|
+
const outcome = await buildTail(false, base, built);
|
|
282
|
+
if (outcome.verdict === "signoff") return;
|
|
283
|
+
await reUnwind(outcome);
|
|
284
|
+
base = outcome.base;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
/** After a tail that skipped planning: stop on sign-off, re-plan on feedback. */
|
|
288
|
+
const afterTail = async (outcome) => {
|
|
289
|
+
if (outcome.verdict === "signoff") return;
|
|
290
|
+
await reUnwind(outcome);
|
|
291
|
+
await planAndBuild(outcome.base);
|
|
292
|
+
};
|
|
293
|
+
/** An ordinary start: wait at `idle`, unwind the sketch, check the baseline, then plan and build. */
|
|
294
|
+
const ordinaryStart = async () => {
|
|
295
|
+
await human("idle", {
|
|
296
|
+
message: idleMessage(),
|
|
297
|
+
label: "Idle",
|
|
298
|
+
file: ".gtd/TODO.md"
|
|
299
|
+
});
|
|
300
|
+
refusePendingHandOff();
|
|
301
|
+
await unwind();
|
|
302
|
+
if (changes(".gtd/FEEDBACK.md").some((c) => c.status !== "deleted")) await human("unwind-failed", {
|
|
303
|
+
message: unwindFailedMessage(),
|
|
304
|
+
label: "Could not unwind your input",
|
|
305
|
+
file: FEEDBACK
|
|
306
|
+
});
|
|
307
|
+
await gate("start-gate", startGateBlockedMessage());
|
|
308
|
+
await planAndBuild(start());
|
|
309
|
+
};
|
|
310
|
+
const feature = shipped(ordinaryStart);
|
|
311
|
+
//#endregion
|
|
312
|
+
//#region src/workflows/feature/index.ts
|
|
313
|
+
var feature_default = feature;
|
|
314
|
+
//#endregion
|
|
315
|
+
export { SCENARIO_PACKAGE, access, afterTail, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, build, buildTail, collecting, declaredTests, decompose, feature_default as default, defaults, design, envDefaults, feature, fixNits, fixSuite, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, reUnwind, requireDeclaredTests, skills, steering, summary, triage, unwind };
|