@pmelab/gtd 57.0.0 → 58.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 +12 -10
- package/dist/chunks/{Schema-C6fLlQzx.mjs → Schema-B3o6eRNU.mjs} +637 -3957
- package/dist/chunks/{reserved-X2RkNHin.mjs → reserved-lMYCaapY.mjs} +27 -20
- package/dist/chunks/{review-v7sh5GF4.mjs → review-Bfaror38.mjs} +2 -2
- package/dist/chunks/{tail-S04KEewi.mjs → tail-Bd7L940p.mjs} +1 -1
- package/dist/chunks/testkit-D1-auYau.mjs +10362 -0
- package/dist/gtd.bundle.mjs +8 -8
- package/dist/testkit/gherkin.mjs +255 -0
- package/dist/testkit.mjs +2 -15461
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +52 -13
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +3 -3
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +74 -28
- package/package.json +8 -1
- package/src/cli/Cli.test.ts +1 -1
- package/src/cli/History.test.ts +1 -1
- package/src/cli/Metrics.test.ts +1 -1
- package/src/cli/Program.test.ts +1 -1
- package/src/cli/Strays.test.ts +1 -1
- package/src/edge/Edge.test.ts +1 -1
- package/src/emit/Emit.ts +1 -1
- package/src/emit/GitScript.ts +1 -1
- package/src/emit/OutcomeScript.ts +1 -1
- package/src/{testkit/steps.ts → gherkin/index.ts} +20 -3
- package/src/gherkin/module-config.feature +24 -0
- package/src/gherkin/tiny.fixture.ts +8 -0
- package/src/platform/Git.test.ts +2 -2
- package/src/platform/RetainedHistory.test.ts +2 -2
- package/src/platform/Workspace.test.ts +2 -2
- package/src/platform/WorktreeState.test.ts +1 -1
- package/src/step/planStart.test.ts +1 -1
- package/src/testkit/TestRepo.ts +7 -4
- package/src/testkit/index.ts +15 -9
- package/src/{testkit → twins}/Layers.ts +2 -2
- package/src/twins/index.ts +8 -0
- package/src/workflow/discovery.test.ts +1 -1
- package/src/workflow/discovery.ts +3 -3
- package/src/workflow/load.ts +3 -3
- package/src/workflows/feature/acceptance-criteria.feature +32 -0
- package/src/workflows/feature/criteria-coverage.feature +0 -1
- package/src/workflows/feature/criteria.ts +10 -4
- package/src/workflows/feature/fast-path.feature +4 -0
- package/src/workflows/feature/feature.test.ts +3 -39
- package/src/workflows/feature/feature.ts +3 -4
- package/src/workflows/feature/lens-models.feature +0 -1
- package/src/workflows/feature/lens-review.feature +0 -1
- package/src/workflows/feature/naive-acceptance.feature +4 -0
- package/src/workflows/feature/naive-test.feature +11 -6
- package/src/workflows/feature/one-step-land.feature +4 -0
- package/src/workflows/feature/planning.ts +2 -0
- package/src/workflows/feature/refactor-absorbs.feature +4 -0
- package/src/workflows/feature/refactor-first.feature +4 -0
- package/src/workflows/feature/refactor-lens-models.feature +4 -0
- package/src/workflows/feature/refactor-rebuild.feature +4 -0
- package/src/workflows/feature/refactor-review.feature +20 -0
- package/src/workflows/feature/release-flag.feature +167 -0
- package/src/workflows/feature/release-flag.test.ts +49 -0
- package/src/workflows/feature/release-flag.ts +46 -0
- package/src/workflows/feature/review-gate.feature +0 -1
- package/src/workflows/feature/reviewer-walkthrough.feature +0 -1
- package/src/workflows/feature/risk-tier.feature +32 -0
- package/src/workflows/feature/seam-rejection.feature +8 -0
- package/src/workflows/feature/seam.feature +8 -0
- package/src/workflows/feature/seam.test.ts +20 -7
- package/src/workflows/feature/seam.ts +7 -7
- package/src/workflows/feature/standards.feature +0 -1
- package/src/workflows/shared/options.test.ts +3 -10
- package/src/workflows/shared/options.ts +0 -9
- package/src/workflows/shared/text.test.ts +36 -28
- package/src/workflows/shared/text.ts +37 -20
- package/src/workflows/shared/vars.ts +0 -1
- package/src/workflows/feature/flag-mode.feature +0 -88
- /package/src/{testkit → gherkin}/testkit.feature +0 -0
- /package/src/{testkit → twins}/EmittedScriptRecognizer.test.ts +0 -0
- /package/src/{testkit → twins}/EmittedScriptRecognizer.ts +0 -0
- /package/src/{testkit → twins}/EmittedScripts.test.ts +0 -0
- /package/src/{testkit → twins}/FakeGitOperations.ts +0 -0
- /package/src/{testkit → twins}/FlowScripts.test.ts +0 -0
- /package/src/{testkit → twins}/FlowScripts.ts +0 -0
- /package/src/{testkit → twins}/GitTiers.test.ts +0 -0
- /package/src/{testkit → twins}/GitTiers.ts +0 -0
- /package/src/{testkit → twins}/InMemRepo.ts +0 -0
- /package/src/{testkit → twins}/Layers.test.ts +0 -0
- /package/src/{testkit → twins}/StrictGitOperations.ts +0 -0
- /package/src/{testkit → twins}/cliIo.ts +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { agent, changes, glob, read, refuse, scope, start, vars } from "../flows.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { It as registryEntry, Lt as requireFlag, Pt as flagReferences, St as envDefaults, a as flagShipped, i as skills, n as summary, r as access, t as steering, x as coder, xt as defaults } from "../chunks/reserved-lMYCaapY.mjs";
|
|
3
|
+
import { a as refusePendingHandOff, r as handOffTail } from "../chunks/tail-Bd7L940p.mjs";
|
|
4
4
|
//#region src/workflows/cleanup/cleanup.ts
|
|
5
5
|
const removingPrompt = (name, registry) => `Clean up the release flag "${name}", which is default-on: delete its entry in ${registry}, and remove every \`GTD_FLAG_${name.toUpperCase()}\` check from the shipped sources. Keep the flag-on path; delete code only the off path reached, including tests that pin the off path.`;
|
|
6
6
|
const cleanupFlow = async () => {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { agent, answered, applyScript, changes, changesSince, check, codeChanges, env, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireThreadsClosed, restoreScript, revertScript, run, scope, start, vars, wrote } from "../flows.mjs";
|
|
2
2
|
import { lensEntriesOf } from "../workflow-kit.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { a as refusePendingHandOff } from "../chunks/tail-
|
|
5
|
-
import { n as isCode$1, r as reUnwind, t as buildTail } from "../chunks/review-
|
|
3
|
+
import { B as designSystem, Ct as testFileGlobs, D as planner, F as clearCriteriaMessage, I as clearCriteriaQuestion, J as implementBuildingPrompt, L as criterionLine, M as buildReviewCollectingPrompt, P as builderSystem, Q as naiveTryMessage, R as designGateAnswerMessage, S as collecting, St as envDefaults, V as designTriagePrompt, X as naiveApplyFailure, Y as implementFixPrompt, Z as naivePassPrompt, _ as REQUIREMENTS, _t as unwindFailedMessage, at as refactorFixPrompt, d as healthy, et as normaliseCriteriaScript, f as FEEDBACK, g as REFACTOR_NOTE, gt as startGateBlockedMessage, h as REFACTOR, ht as seamSteerMessage, i as skills, it as refactorDroppedRefusal, j as agentWithSkills, l as gate, mt as seamByHandMessage, n as summary, o as naiveTestOn, ot as refactorNotePrompt, p as NAIVE_TEST, pt as reviewerSystem, q as idleMessage, r as access, rt as refactorBuildingPrompt, s as shipped, st as refactorRules, t as steering, vt as unwindFailure, w as fixNits, x as coder, xt as defaults, y as answerReviewQuestions, z as designImpactPrompt } from "../chunks/reserved-lMYCaapY.mjs";
|
|
4
|
+
import { a as refusePendingHandOff } from "../chunks/tail-Bd7L940p.mjs";
|
|
5
|
+
import { n as isCode$1, r as reUnwind, t as buildTail } from "../chunks/review-Bfaror38.mjs";
|
|
6
6
|
import { posix } from "node:path";
|
|
7
7
|
//#region src/workflows/shared/fences.ts
|
|
8
8
|
const markFenced = (text) => {
|
|
@@ -23,6 +23,43 @@ const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
|
23
23
|
/** A list item, its checkbox dropped: its text. */
|
|
24
24
|
const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
|
|
25
25
|
//#endregion
|
|
26
|
+
//#region src/workflows/feature/release-flag.ts
|
|
27
|
+
const NAME = /^existing:\s*`?([A-Za-z_][\w.-]*)`?$/i;
|
|
28
|
+
const isFlagHeading = (h) => h !== null && h[1].length === 2 && /^release flag$/i.test(h[2].trim());
|
|
29
|
+
/** The first line under `## Release Flag`, `undefined` without the section. */
|
|
30
|
+
const requestLine = (text) => {
|
|
31
|
+
const lines = markFenced(text);
|
|
32
|
+
const at = lines.findIndex((l) => !l.fenced && isFlagHeading(HEADING.exec(l.line)));
|
|
33
|
+
if (at === -1) return void 0;
|
|
34
|
+
const body = lines.slice(at + 1);
|
|
35
|
+
const end = body.findIndex((l) => !l.fenced && HEADING.test(l.line));
|
|
36
|
+
return (end === -1 ? body : body.slice(0, end)).find((l) => l.line.trim() !== "")?.line.trim();
|
|
37
|
+
};
|
|
38
|
+
/** What `text` asks for, or why it cannot be read. No section asks for no flag. */
|
|
39
|
+
const releaseFlagOf = (text) => {
|
|
40
|
+
const line = requestLine(text);
|
|
41
|
+
if (line === void 0 || /^none$/i.test(line)) return { kind: "none" };
|
|
42
|
+
if (/^new$/i.test(line)) return { kind: "new" };
|
|
43
|
+
const name = NAME.exec(line)?.[1];
|
|
44
|
+
return name === void 0 ? { unreadable: line } : {
|
|
45
|
+
kind: "existing",
|
|
46
|
+
name
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
/** Refuse `file` when its `## Release Flag` section is neither `none`, `new` nor `existing: <NAME>`. */
|
|
50
|
+
const requireReleaseFlag = (file) => {
|
|
51
|
+
const request = releaseFlagOf(read(file) ?? "");
|
|
52
|
+
if (!("unreadable" in request)) return;
|
|
53
|
+
refuse(`gtd land: release-flag: ${file}'s "## Release Flag" reads ${JSON.stringify(request.unreadable)} — its first line must be \`new\`, \`existing: <NAME>\` or \`none\`, or leave the section out for no flag`);
|
|
54
|
+
};
|
|
55
|
+
/** What `.gtd/REQUIREMENTS.md` asks for now; a person's unreadable edit is refused. */
|
|
56
|
+
const requestedFlag = () => {
|
|
57
|
+
requireReleaseFlag(REQUIREMENTS);
|
|
58
|
+
return releaseFlagOf(read(".gtd/REQUIREMENTS.md") ?? "");
|
|
59
|
+
};
|
|
60
|
+
/** Whether the change goes behind a release flag: only then does the seam judge rule. */
|
|
61
|
+
const flagged = () => requestedFlag().kind !== "none";
|
|
62
|
+
//#endregion
|
|
26
63
|
//#region src/workflows/feature/criteria.ts
|
|
27
64
|
/** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
|
|
28
65
|
const CLEAR_MIN_P = .8;
|
|
@@ -114,14 +151,15 @@ const requireCriteria = (file) => {
|
|
|
114
151
|
refuse(`gtd land: acceptance-criteria: ${file} has ${criteria.length === 0 ? `no "## Acceptance Criteria" list` : unnamed.length > 0 ? `${unnamed.length} criterion without an ID:\n${unnamed.map((c) => ` - ${c.text}`).join("\n")}` : `duplicate IDs: ${[...new Set(twice)].join(", ")}`} — list every criterion under "## Acceptance Criteria" as "- AC-<n>: <checkable criterion>", each ID unique and never reused`);
|
|
115
152
|
};
|
|
116
153
|
const isImpactHeading = (h) => h !== null && h[1].length === 2 && /^impact$/i.test(h[2].trim());
|
|
154
|
+
const isImpactTurnHeading = (h) => h !== null && h[1].length === 2 && /^(impact|release flag)$/i.test(h[2].trim());
|
|
117
155
|
/** Whether `text` has an unfenced, level-2 `## Impact` heading. */
|
|
118
156
|
const hasImpact = (text) => markFenced(text).some((l) => !l.fenced && isImpactHeading(HEADING.exec(l.line)));
|
|
119
|
-
/** `text` without its `## Impact`
|
|
157
|
+
/** `text` without its `## Impact` and `## Release Flag` sections, whitespace collapsed. */
|
|
120
158
|
const withoutImpact = (text) => {
|
|
121
159
|
let inImpact = false;
|
|
122
160
|
return markFenced(text).filter((l) => {
|
|
123
161
|
const h = l.fenced ? null : HEADING.exec(l.line);
|
|
124
|
-
if (h !== null && h[1].length <= 2) inImpact =
|
|
162
|
+
if (h !== null && h[1].length <= 2) inImpact = isImpactTurnHeading(h);
|
|
125
163
|
return !inImpact;
|
|
126
164
|
}).map((l) => l.line).join(" ").replace(/\s+/g, " ").trim();
|
|
127
165
|
};
|
|
@@ -138,12 +176,12 @@ const requireImpact = (file) => {
|
|
|
138
176
|
if (hasImpact(text)) return;
|
|
139
177
|
refuse(`gtd land: impact: ${file} has no "## Impact" section — explore the codebase and add "## Impact" naming the units, public surfaces and surprises this change touches`);
|
|
140
178
|
};
|
|
141
|
-
/** Refuse a turn that changed `file` beyond its `## Impact`
|
|
179
|
+
/** Refuse a turn that changed `file` beyond its `## Impact` and `## Release Flag` sections. */
|
|
142
180
|
const requireImpactOnly = (file) => {
|
|
143
181
|
const change = changes().get(file);
|
|
144
182
|
if (change === void 0) return;
|
|
145
183
|
if (withoutImpact(change.before ?? "") === withoutImpact(change.after ?? "")) return;
|
|
146
|
-
refuse(`gtd land: impact-only: this turn may only add "## Impact" to ${file} — restore everything else`);
|
|
184
|
+
refuse(`gtd land: impact-only: this turn may only add "## Impact" (and "## Release Flag") to ${file} — restore everything else`);
|
|
147
185
|
};
|
|
148
186
|
const idNumber = (id) => Number(id.slice(3));
|
|
149
187
|
/** Why `listed` renumbers or reuses an earlier ID; `undefined` when it does neither. */
|
|
@@ -190,6 +228,7 @@ const impact = async () => {
|
|
|
190
228
|
requireCriteriaFrozen(REQUIREMENTS);
|
|
191
229
|
requireImpact(REQUIREMENTS);
|
|
192
230
|
requireImpactOnly(REQUIREMENTS);
|
|
231
|
+
requireReleaseFlag(REQUIREMENTS);
|
|
193
232
|
};
|
|
194
233
|
/**
|
|
195
234
|
* The skip path: when the sketch stated clear, checkable criteria, write them
|
|
@@ -293,6 +332,7 @@ const design = (base, guard) => scope("design", async () => {
|
|
|
293
332
|
requireReplies(REQUIREMENTS);
|
|
294
333
|
requireCriteria(REQUIREMENTS);
|
|
295
334
|
requireImpact(REQUIREMENTS);
|
|
335
|
+
requireReleaseFlag(REQUIREMENTS);
|
|
296
336
|
guard?.();
|
|
297
337
|
} while (await noteGate(REQUIREMENTS, answerProductQuestions));
|
|
298
338
|
});
|
|
@@ -658,7 +698,7 @@ const applied = async (tip) => {
|
|
|
658
698
|
return !wrote(".gtd/FEEDBACK.md") && codeChanges().length > 0;
|
|
659
699
|
};
|
|
660
700
|
const build = async (options) => {
|
|
661
|
-
await agentWithSkills("building", implementBuildingPrompt(options.reference, options.
|
|
701
|
+
await agentWithSkills("building", implementBuildingPrompt(options.reference, options.amend, options.afterRefactor, requestedFlag(), options.rebuild), {
|
|
662
702
|
label: "Implementing",
|
|
663
703
|
model: coder(),
|
|
664
704
|
system: builderSystem(),
|
|
@@ -685,7 +725,7 @@ const implementing = (options) => scope("implement", async () => {
|
|
|
685
725
|
/** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
|
|
686
726
|
const steer = async (retry) => {
|
|
687
727
|
await human("steer", {
|
|
688
|
-
message: seamSteerMessage(retry,
|
|
728
|
+
message: seamSteerMessage(retry, flagged()),
|
|
689
729
|
label: "Steer or overrule the naive pass",
|
|
690
730
|
file: REQUIREMENTS,
|
|
691
731
|
acceptClean: true
|
|
@@ -764,7 +804,7 @@ const tried = async (tip) => {
|
|
|
764
804
|
const before = head();
|
|
765
805
|
await run("applying", applyScript(tip.hash, FEEDBACK, naiveApplyFailure(tip.hash), [".gtd"]), { label: "Applying the naive pass to try it" });
|
|
766
806
|
await human("try", {
|
|
767
|
-
message: naiveTryMessage(
|
|
807
|
+
message: naiveTryMessage(flagged()),
|
|
768
808
|
label: "Try the naive pass",
|
|
769
809
|
file: NAIVE_TEST,
|
|
770
810
|
acceptClean: true
|
|
@@ -798,7 +838,7 @@ const decide = () => scope("seam", async () => {
|
|
|
798
838
|
continue;
|
|
799
839
|
}
|
|
800
840
|
}
|
|
801
|
-
const outcome = verdict ?? (
|
|
841
|
+
const outcome = verdict ?? (flagged() ? await ruled(tip, vetting.again) : {
|
|
802
842
|
reference: tip.hash,
|
|
803
843
|
tip
|
|
804
844
|
});
|
|
@@ -867,7 +907,7 @@ const unwind = () => {
|
|
|
867
907
|
return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
|
|
868
908
|
};
|
|
869
909
|
/**
|
|
870
|
-
* A naive pass (judged only
|
|
910
|
+
* A naive pass (judged only when a release flag is asked for), then implement and the
|
|
871
911
|
* quality lap, then the review; design must have run since `firstBase`.
|
|
872
912
|
*/
|
|
873
913
|
const buildFeature = async (firstBase) => {
|
|
@@ -959,7 +999,6 @@ const ordinaryStart = async () => {
|
|
|
959
999
|
file: ".gtd/TODO.md"
|
|
960
1000
|
});
|
|
961
1001
|
refusePendingHandOff();
|
|
962
|
-
flagModeOn();
|
|
963
1002
|
naiveTestOn();
|
|
964
1003
|
const stated = statedCriteria(changes(), head());
|
|
965
1004
|
await unwind();
|
package/dist/workflows/fix.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
|
|
2
2
|
import { b as reviewNotes, x as reviewRisks } from "../chunks/steering-CCNvngSH.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-
|
|
3
|
+
import { G as fixReviewNotesPrompt, St as envDefaults, W as finisherSystem, c as baseline, i as skills, j as agentWithSkills, n as summary, r as access, s as shipped, t as steering, v as REVIEW, x as coder, xt as defaults } from "../chunks/reserved-lMYCaapY.mjs";
|
|
4
|
+
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-Bd7L940p.mjs";
|
|
5
5
|
//#region src/workflows/fix/fix.ts
|
|
6
6
|
/**
|
|
7
7
|
* Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, start } from "../flows.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { a as refusePendingHandOff } from "../chunks/tail-
|
|
4
|
-
import { r as reUnwind, t as buildTail } from "../chunks/review-
|
|
2
|
+
import { St as envDefaults, W as finisherSystem, _ as REQUIREMENTS, dt as reviewGateBlockedMessage, i as skills, j as agentWithSkills, l as gate, n as summary, r as access, s as shipped, t as steering, ut as reviewFeedbackFixPrompt, x as coder, xt as defaults } from "../chunks/reserved-lMYCaapY.mjs";
|
|
3
|
+
import { a as refusePendingHandOff } from "../chunks/tail-Bd7L940p.mjs";
|
|
4
|
+
import { r as reUnwind, t as buildTail } from "../chunks/review-Bfaror38.mjs";
|
|
5
5
|
//#region src/workflows/review/review.ts
|
|
6
6
|
const fixFeedback = async () => {
|
|
7
7
|
await agentWithSkills("feedback.fixing", reviewFeedbackFixPrompt(), {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { Ft as flipProblems, Lt as requireFlag, Nt as flagMentions, St as envDefaults, a as flagShipped, f as FEEDBACK, i as skills, l as gate, n as summary, r as access, t as steering, x as coder, xt as defaults } from "../chunks/reserved-lMYCaapY.mjs";
|
|
3
3
|
//#region src/workflows/rollout/rollout.ts
|
|
4
4
|
const flippingPrompt = (name, registry) => `Roll out the release flag "${name}": in ${registry}, set \`defaultOn: true\` on its entry. Change nothing else — no other entry, no other field, no other file, no comments.`;
|
|
5
5
|
const suiteBlockedMessage = () => "The suite is red after the rollout. Fix it by hand — a rollout may only touch the flag's registry entry — then land.";
|
package/dist/workflows.mjs
CHANGED
|
@@ -17636,7 +17636,6 @@ const defaults = {
|
|
|
17636
17636
|
standardMinP: "0.8",
|
|
17637
17637
|
refactorFlagPattern: "\\bGTD_FLAG_\\w+",
|
|
17638
17638
|
seamAllow: [...testFileGlobs, ".gtd/**"].join(", "),
|
|
17639
|
-
flagMode: "true",
|
|
17640
17639
|
naiveTest: "false",
|
|
17641
17640
|
flag_risk_tier: "0",
|
|
17642
17641
|
qualityReviews: "correctness, owasp-security, ponytail-review, test-audit, conventions, spec-challenge, criteria-coverage",
|
|
@@ -18070,6 +18069,18 @@ in its docs or agent instructions (\`AGENTS.md\`, \`CLAUDE.md\`). When
|
|
|
18070
18069
|
none is declared, fall back to directories and packages. Landing is
|
|
18071
18070
|
refused while the section is missing.
|
|
18072
18071
|
`;
|
|
18072
|
+
const releaseFlagBlock = `
|
|
18073
|
+
## Release flag
|
|
18074
|
+
|
|
18075
|
+
Only when the sketch asks outright for the change to ship behind a
|
|
18076
|
+
feature or release flag, \`.gtd/REQUIREMENTS.md\` carries a
|
|
18077
|
+
\`## Release Flag\` section whose first line is \`new\` (a new
|
|
18078
|
+
flag) or \`existing: <NAME>\` (behind a flag that already exists,
|
|
18079
|
+
named as the sketch names it). Never add one on your own judgement:
|
|
18080
|
+
without the section the change ships unflagged. A later lap keeps the
|
|
18081
|
+
section exactly as the last settled file had it, unless the human
|
|
18082
|
+
changed it. Landing is refused while its first line is anything else.
|
|
18083
|
+
`;
|
|
18073
18084
|
const riskTierBlock = `
|
|
18074
18085
|
## Risk tier and production signals
|
|
18075
18086
|
|
|
@@ -18109,7 +18120,7 @@ ${footnoteFoldIn}${codeThreadReplies()}
|
|
|
18109
18120
|
or both
|
|
18110
18121
|
- Whichever lap this is, leave \`.gtd/REQUIREMENTS.md\`
|
|
18111
18122
|
uncommitted and finish once it reflects this lap's own work
|
|
18112
|
-
${acceptanceCriteriaBlock}${impactBlock}
|
|
18123
|
+
${acceptanceCriteriaBlock}${impactBlock}${releaseFlagBlock}
|
|
18113
18124
|
Every lap re-explores and leaves \`## Impact\` present. On a return
|
|
18114
18125
|
lap or review loop-back the human may have edited \`## Impact\` (or
|
|
18115
18126
|
footnoted it): read that edit from history (\`git log -1 -p --
|
|
@@ -18179,11 +18190,12 @@ ${stateFileRules}
|
|
|
18179
18190
|
- The only state file this turn touches is \`.gtd/REQUIREMENTS.md\`
|
|
18180
18191
|
— no other files for notes or output
|
|
18181
18192
|
- \`.gtd/REQUIREMENTS.md\` already holds the settled acceptance
|
|
18182
|
-
criteria. Add a \`## Impact\` section and
|
|
18183
|
-
|
|
18193
|
+
criteria. Add a \`## Impact\` section — and a \`## Release Flag\`
|
|
18194
|
+
section when the sketch asks for one — and change nothing else: a
|
|
18195
|
+
turn that touches anything outside them is refused
|
|
18184
18196
|
- No human reviews this before the build: put any surprise in the
|
|
18185
18197
|
Impact prose, never as a question
|
|
18186
|
-
${impactBlock}
|
|
18198
|
+
${impactBlock}${releaseFlagBlock}
|
|
18187
18199
|
- Leave \`.gtd/REQUIREMENTS.md\` uncommitted and finish once it holds \`## Impact\`
|
|
18188
18200
|
`;
|
|
18189
18201
|
const clearCriteriaQuestion = {
|
|
@@ -18242,6 +18254,7 @@ What each change does next (then run \`gtd land\`):
|
|
|
18242
18254
|
- **Accept as-is** — change nothing and re-run; the plan stands as written. Refused while a thread is open.
|
|
18243
18255
|
- **Revise answers** — tick exactly one option per open question (replace \`_your answer_\` for your own) to send it back for the agent to fold your answers in, or delete a question to skip it.
|
|
18244
18256
|
- **Discuss** — start or continue a thread (\`- H:\`); the agent replies and this gate stops again.
|
|
18257
|
+
- **Ask for a release flag** — add \`## Release Flag\` with \`new\` or \`existing: <NAME>\` as its first line, or delete the section to ship unflagged.
|
|
18245
18258
|
${riskTier ? RISK_TIER_GATE_LINE : ""}`;
|
|
18246
18259
|
const builderSystem = () => `${builderPersona}
|
|
18247
18260
|
|
|
@@ -18921,21 +18934,22 @@ const afterRefactorLine = (rebuild) => `- A reviewed refactor has just landed an
|
|
|
18921
18934
|
though never a release flag. Build only what is still missing, held to every
|
|
18922
18935
|
rule below; if nothing is, ${carries(rebuild) ? "carry over the acceptance tests below and change nothing else" : "change nothing and finish your turn"}
|
|
18923
18936
|
`;
|
|
18924
|
-
const flagLine = (
|
|
18925
|
-
|
|
18926
|
-
|
|
18937
|
+
const flagLine = (flag, amend) => flag.kind === "none" ? "- Add no release flag — the requirements ask for none" : flag.kind === "existing" ? `- Put the new behaviour behind the existing flag \`${flag.name}\`, as the
|
|
18938
|
+
requirements ask — add no new flag` : amend === true ? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag" : `- Put the new behaviour behind a new release flag, as the requirements ask,
|
|
18939
|
+
following the repository's flag conventions`;
|
|
18940
|
+
const implementBuildingPrompt = (reference, amend, afterRefactor, flag = { kind: "none" }, rebuild = {}) => `${stateFileRules}
|
|
18927
18941
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18928
18942
|
${impactHints}
|
|
18929
18943
|
${criteriaBinding(true)}
|
|
18930
18944
|
${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
|
|
18931
18945
|
the re-derived requirements instead of rewriting it
|
|
18932
|
-
` :
|
|
18946
|
+
` : flag.kind !== "none" ? `- Implement them properly, fitting the codebase's seams and conventions
|
|
18933
18947
|
` : `- Implement them the quickest way that works — the naive version. Do not
|
|
18934
18948
|
restructure, abstract or refactor around the change
|
|
18935
18949
|
`}${reference === void 0 || amend === true ? "" : `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
|
|
18936
18950
|
Read it for ideas; never apply, cherry-pick or check it out — write
|
|
18937
18951
|
the change fresh${carries(rebuild) ? ", its acceptance tests excepted (below)" : ""}
|
|
18938
|
-
`}${rebuildLines(rebuild)}${flagLine(
|
|
18952
|
+
`}${rebuildLines(rebuild)}${flagLine(flag, amend)}
|
|
18939
18953
|
- Write the tests first, then the code that makes them pass
|
|
18940
18954
|
${behaviourTests}
|
|
18941
18955
|
- Leave \`.gtd/REQUIREMENTS.md\` in place and everything uncommitted, then
|
|
@@ -18954,7 +18968,7 @@ An edit together with an approval counts as steer. Only
|
|
|
18954
18968
|
\`.gtd/REQUIREMENTS.md\` is meant to change here: any other edit lands on the
|
|
18955
18969
|
branch unjudged.
|
|
18956
18970
|
`;
|
|
18957
|
-
/** `judged`: whether an overruled pass still meets the seam judge (
|
|
18971
|
+
/** `judged`: whether an overruled pass still meets the seam judge (a release flag asked for). */
|
|
18958
18972
|
const seamSteerMessage = (retry, judged = true) => isFault(retry) ? `The naive pass failed three times and the retries are spent. The last
|
|
18959
18973
|
attempt's tests (\`git show ${retry.hash}\`) fall short:
|
|
18960
18974
|
${retry.fault}
|
|
@@ -38602,19 +38616,12 @@ const ShippedOptions = Struct({
|
|
|
38602
38616
|
testCommand: setting,
|
|
38603
38617
|
plannerModel: setting,
|
|
38604
38618
|
coderModel: setting,
|
|
38605
|
-
flagMode: optionalWith(Boolean$, { exact: true }),
|
|
38606
38619
|
naiveTest: optionalWith(Boolean$, { exact: true })
|
|
38607
38620
|
});
|
|
38608
38621
|
const over = (base, options) => ({
|
|
38609
38622
|
...base,
|
|
38610
38623
|
...Object.fromEntries(Object.entries(options).filter(([name]) => Object.hasOwn(base, name)).map(([name, value]) => [name, String(value)]))
|
|
38611
38624
|
});
|
|
38612
|
-
/** The `flagMode` setting, pinned at process start. Anything but true/false fails rather than guess a side. */
|
|
38613
|
-
const flagModeOn = () => {
|
|
38614
|
-
const value = vars.flagMode ?? defaults.flagMode;
|
|
38615
|
-
if (value !== "true" && value !== "false") refuse(`gtd: flagMode must be "true" or "false", got ${JSON.stringify(value)}`);
|
|
38616
|
-
return value === "true";
|
|
38617
|
-
};
|
|
38618
38625
|
/** The `naiveTest` setting, pinned at process start. Anything but true/false fails rather than guess. */
|
|
38619
38626
|
const naiveTestOn = () => {
|
|
38620
38627
|
const value = vars.naiveTest ?? defaults.naiveTest;
|
|
@@ -39032,6 +39039,43 @@ const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
|
39032
39039
|
/** A list item, its checkbox dropped: its text. */
|
|
39033
39040
|
const ITEM = /^ ?(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.*)$/;
|
|
39034
39041
|
//#endregion
|
|
39042
|
+
//#region src/workflows/feature/release-flag.ts
|
|
39043
|
+
const NAME = /^existing:\s*`?([A-Za-z_][\w.-]*)`?$/i;
|
|
39044
|
+
const isFlagHeading = (h) => h !== null && h[1].length === 2 && /^release flag$/i.test(h[2].trim());
|
|
39045
|
+
/** The first line under `## Release Flag`, `undefined` without the section. */
|
|
39046
|
+
const requestLine = (text) => {
|
|
39047
|
+
const lines = markFenced(text);
|
|
39048
|
+
const at = lines.findIndex((l) => !l.fenced && isFlagHeading(HEADING.exec(l.line)));
|
|
39049
|
+
if (at === -1) return void 0;
|
|
39050
|
+
const body = lines.slice(at + 1);
|
|
39051
|
+
const end = body.findIndex((l) => !l.fenced && HEADING.test(l.line));
|
|
39052
|
+
return (end === -1 ? body : body.slice(0, end)).find((l) => l.line.trim() !== "")?.line.trim();
|
|
39053
|
+
};
|
|
39054
|
+
/** What `text` asks for, or why it cannot be read. No section asks for no flag. */
|
|
39055
|
+
const releaseFlagOf = (text) => {
|
|
39056
|
+
const line = requestLine(text);
|
|
39057
|
+
if (line === void 0 || /^none$/i.test(line)) return { kind: "none" };
|
|
39058
|
+
if (/^new$/i.test(line)) return { kind: "new" };
|
|
39059
|
+
const name = NAME.exec(line)?.[1];
|
|
39060
|
+
return name === void 0 ? { unreadable: line } : {
|
|
39061
|
+
kind: "existing",
|
|
39062
|
+
name
|
|
39063
|
+
};
|
|
39064
|
+
};
|
|
39065
|
+
/** Refuse `file` when its `## Release Flag` section is neither `none`, `new` nor `existing: <NAME>`. */
|
|
39066
|
+
const requireReleaseFlag = (file) => {
|
|
39067
|
+
const request = releaseFlagOf(read(file) ?? "");
|
|
39068
|
+
if (!("unreadable" in request)) return;
|
|
39069
|
+
refuse(`gtd land: release-flag: ${file}'s "## Release Flag" reads ${JSON.stringify(request.unreadable)} — its first line must be \`new\`, \`existing: <NAME>\` or \`none\`, or leave the section out for no flag`);
|
|
39070
|
+
};
|
|
39071
|
+
/** What `.gtd/REQUIREMENTS.md` asks for now; a person's unreadable edit is refused. */
|
|
39072
|
+
const requestedFlag = () => {
|
|
39073
|
+
requireReleaseFlag(REQUIREMENTS);
|
|
39074
|
+
return releaseFlagOf(read(".gtd/REQUIREMENTS.md") ?? "");
|
|
39075
|
+
};
|
|
39076
|
+
/** Whether the change goes behind a release flag: only then does the seam judge rule. */
|
|
39077
|
+
const flagged = () => requestedFlag().kind !== "none";
|
|
39078
|
+
//#endregion
|
|
39035
39079
|
//#region src/workflows/feature/criteria.ts
|
|
39036
39080
|
/** Below this the judge's `yes` is not trusted: a wrong skip builds against criteria nobody can check. */
|
|
39037
39081
|
const CLEAR_MIN_P = .8;
|
|
@@ -39123,14 +39167,15 @@ const requireCriteria = (file) => {
|
|
|
39123
39167
|
refuse(`gtd land: acceptance-criteria: ${file} has ${criteria.length === 0 ? `no "## Acceptance Criteria" list` : unnamed.length > 0 ? `${unnamed.length} criterion without an ID:\n${unnamed.map((c) => ` - ${c.text}`).join("\n")}` : `duplicate IDs: ${[...new Set(twice)].join(", ")}`} — list every criterion under "## Acceptance Criteria" as "- AC-<n>: <checkable criterion>", each ID unique and never reused`);
|
|
39124
39168
|
};
|
|
39125
39169
|
const isImpactHeading = (h) => h !== null && h[1].length === 2 && /^impact$/i.test(h[2].trim());
|
|
39170
|
+
const isImpactTurnHeading = (h) => h !== null && h[1].length === 2 && /^(impact|release flag)$/i.test(h[2].trim());
|
|
39126
39171
|
/** Whether `text` has an unfenced, level-2 `## Impact` heading. */
|
|
39127
39172
|
const hasImpact = (text) => markFenced(text).some((l) => !l.fenced && isImpactHeading(HEADING.exec(l.line)));
|
|
39128
|
-
/** `text` without its `## Impact`
|
|
39173
|
+
/** `text` without its `## Impact` and `## Release Flag` sections, whitespace collapsed. */
|
|
39129
39174
|
const withoutImpact = (text) => {
|
|
39130
39175
|
let inImpact = false;
|
|
39131
39176
|
return markFenced(text).filter((l) => {
|
|
39132
39177
|
const h = l.fenced ? null : HEADING.exec(l.line);
|
|
39133
|
-
if (h !== null && h[1].length <= 2) inImpact =
|
|
39178
|
+
if (h !== null && h[1].length <= 2) inImpact = isImpactTurnHeading(h);
|
|
39134
39179
|
return !inImpact;
|
|
39135
39180
|
}).map((l) => l.line).join(" ").replace(/\s+/g, " ").trim();
|
|
39136
39181
|
};
|
|
@@ -39147,12 +39192,12 @@ const requireImpact = (file) => {
|
|
|
39147
39192
|
if (hasImpact(text)) return;
|
|
39148
39193
|
refuse(`gtd land: impact: ${file} has no "## Impact" section — explore the codebase and add "## Impact" naming the units, public surfaces and surprises this change touches`);
|
|
39149
39194
|
};
|
|
39150
|
-
/** Refuse a turn that changed `file` beyond its `## Impact`
|
|
39195
|
+
/** Refuse a turn that changed `file` beyond its `## Impact` and `## Release Flag` sections. */
|
|
39151
39196
|
const requireImpactOnly = (file) => {
|
|
39152
39197
|
const change = changes().get(file);
|
|
39153
39198
|
if (change === void 0) return;
|
|
39154
39199
|
if (withoutImpact(change.before ?? "") === withoutImpact(change.after ?? "")) return;
|
|
39155
|
-
refuse(`gtd land: impact-only: this turn may only add "## Impact" to ${file} — restore everything else`);
|
|
39200
|
+
refuse(`gtd land: impact-only: this turn may only add "## Impact" (and "## Release Flag") to ${file} — restore everything else`);
|
|
39156
39201
|
};
|
|
39157
39202
|
const idNumber = (id) => Number(id.slice(3));
|
|
39158
39203
|
/** Why `listed` renumbers or reuses an earlier ID; `undefined` when it does neither. */
|
|
@@ -39199,6 +39244,7 @@ const impact = async () => {
|
|
|
39199
39244
|
requireCriteriaFrozen(REQUIREMENTS);
|
|
39200
39245
|
requireImpact(REQUIREMENTS);
|
|
39201
39246
|
requireImpactOnly(REQUIREMENTS);
|
|
39247
|
+
requireReleaseFlag(REQUIREMENTS);
|
|
39202
39248
|
};
|
|
39203
39249
|
/**
|
|
39204
39250
|
* The skip path: when the sketch stated clear, checkable criteria, write them
|
|
@@ -39302,6 +39348,7 @@ const design = (base, guard) => scope("design", async () => {
|
|
|
39302
39348
|
requireReplies(REQUIREMENTS);
|
|
39303
39349
|
requireCriteria(REQUIREMENTS);
|
|
39304
39350
|
requireImpact(REQUIREMENTS);
|
|
39351
|
+
requireReleaseFlag(REQUIREMENTS);
|
|
39305
39352
|
guard?.();
|
|
39306
39353
|
} while (await noteGate(REQUIREMENTS, answerProductQuestions));
|
|
39307
39354
|
});
|
|
@@ -40088,7 +40135,7 @@ const applied = async (tip) => {
|
|
|
40088
40135
|
return !wrote(".gtd/FEEDBACK.md") && codeChanges().length > 0;
|
|
40089
40136
|
};
|
|
40090
40137
|
const build = async (options) => {
|
|
40091
|
-
await agentWithSkills("building", implementBuildingPrompt(options.reference, options.
|
|
40138
|
+
await agentWithSkills("building", implementBuildingPrompt(options.reference, options.amend, options.afterRefactor, requestedFlag(), options.rebuild), {
|
|
40092
40139
|
label: "Implementing",
|
|
40093
40140
|
model: coder(),
|
|
40094
40141
|
system: builderSystem(),
|
|
@@ -40115,7 +40162,7 @@ const implementing = (options) => scope("implement", async () => {
|
|
|
40115
40162
|
/** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
|
|
40116
40163
|
const steer = async (retry) => {
|
|
40117
40164
|
await human("steer", {
|
|
40118
|
-
message: seamSteerMessage(retry,
|
|
40165
|
+
message: seamSteerMessage(retry, flagged()),
|
|
40119
40166
|
label: "Steer or overrule the naive pass",
|
|
40120
40167
|
file: REQUIREMENTS,
|
|
40121
40168
|
acceptClean: true
|
|
@@ -40194,7 +40241,7 @@ const tried = async (tip) => {
|
|
|
40194
40241
|
const before = head$1();
|
|
40195
40242
|
await run("applying", applyScript(tip.hash, FEEDBACK, naiveApplyFailure(tip.hash), [".gtd"]), { label: "Applying the naive pass to try it" });
|
|
40196
40243
|
await human("try", {
|
|
40197
|
-
message: naiveTryMessage(
|
|
40244
|
+
message: naiveTryMessage(flagged()),
|
|
40198
40245
|
label: "Try the naive pass",
|
|
40199
40246
|
file: NAIVE_TEST,
|
|
40200
40247
|
acceptClean: true
|
|
@@ -40228,7 +40275,7 @@ const decide = () => scope("seam", async () => {
|
|
|
40228
40275
|
continue;
|
|
40229
40276
|
}
|
|
40230
40277
|
}
|
|
40231
|
-
const outcome = verdict ?? (
|
|
40278
|
+
const outcome = verdict ?? (flagged() ? await ruled(tip, vetting.again) : {
|
|
40232
40279
|
reference: tip.hash,
|
|
40233
40280
|
tip
|
|
40234
40281
|
});
|
|
@@ -40297,7 +40344,7 @@ const unwind = () => {
|
|
|
40297
40344
|
return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
|
|
40298
40345
|
};
|
|
40299
40346
|
/**
|
|
40300
|
-
* A naive pass (judged only
|
|
40347
|
+
* A naive pass (judged only when a release flag is asked for), then implement and the
|
|
40301
40348
|
* quality lap, then the review; design must have run since `firstBase`.
|
|
40302
40349
|
*/
|
|
40303
40350
|
const buildFeature = async (firstBase) => {
|
|
@@ -40389,7 +40436,6 @@ const ordinaryStart = async () => {
|
|
|
40389
40436
|
file: ".gtd/TODO.md"
|
|
40390
40437
|
});
|
|
40391
40438
|
refusePendingHandOff();
|
|
40392
|
-
flagModeOn();
|
|
40393
40439
|
naiveTestOn();
|
|
40394
40440
|
const stated = statedCriteria(changes(), head$1());
|
|
40395
40441
|
await unwind();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmelab/gtd",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "58.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
|
|
6
6
|
"bin": {
|
|
@@ -66,6 +66,11 @@
|
|
|
66
66
|
"types": "./src/testkit/index.ts",
|
|
67
67
|
"source": "./src/testkit/index.ts",
|
|
68
68
|
"default": "./dist/testkit.mjs"
|
|
69
|
+
},
|
|
70
|
+
"./testkit/gherkin": {
|
|
71
|
+
"types": "./src/gherkin/index.ts",
|
|
72
|
+
"source": "./src/gherkin/index.ts",
|
|
73
|
+
"default": "./dist/testkit/gherkin.mjs"
|
|
69
74
|
}
|
|
70
75
|
},
|
|
71
76
|
"files": [
|
|
@@ -76,6 +81,8 @@
|
|
|
76
81
|
"src/wire/",
|
|
77
82
|
"src/steering/",
|
|
78
83
|
"src/testkit/",
|
|
84
|
+
"src/twins/",
|
|
85
|
+
"src/gherkin/",
|
|
79
86
|
"src/cli/",
|
|
80
87
|
"src/edge/",
|
|
81
88
|
"src/emit/",
|
package/src/cli/Cli.test.ts
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
import { EXIT_CODES, EXIT_RUNTIME_ERROR, EXIT_USAGE_ERROR } from "../wire/index.js"
|
|
26
26
|
import { Host } from "../platform/index.js"
|
|
27
27
|
import { type CommandRequirements } from "./Cli.js"
|
|
28
|
-
import { InMemRepo, makeCapturingCliIo, testLayers } from "../
|
|
28
|
+
import { InMemRepo, makeCapturingCliIo, testLayers } from "../twins/index.js"
|
|
29
29
|
import { CONFIG_SCAFFOLD } from "../install/index.js"
|
|
30
30
|
|
|
31
31
|
const FLAG_NAMES = [
|
package/src/cli/History.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect, Exit } from "effect"
|
|
2
2
|
import { describe, expect, it } from "vitest"
|
|
3
|
-
import { applyEmittedScript, InMemRepo, makeCapturingCliIo, testLayers } from "../
|
|
3
|
+
import { applyEmittedScript, InMemRepo, makeCapturingCliIo, testLayers } from "../twins/index.js"
|
|
4
4
|
import { runCli } from "./index.js"
|
|
5
5
|
import { historyEntries, resolveHistoryRef, type HistoryEntry } from "./History.js"
|
|
6
6
|
|
package/src/cli/Metrics.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
2
|
import { describe, expect, it } from "vitest"
|
|
3
|
-
import { applyEmittedScript, InMemRepo, makeCapturingCliIo, testLayers } from "../
|
|
3
|
+
import { applyEmittedScript, InMemRepo, makeCapturingCliIo, testLayers } from "../twins/index.js"
|
|
4
4
|
import { runCli } from "./index.js"
|
|
5
5
|
import { processMetrics, readProcessMetrics, totalMetrics, type MetricCommit } from "./Metrics.js"
|
|
6
6
|
|
package/src/cli/Program.test.ts
CHANGED
|
@@ -18,7 +18,7 @@ vi.mock("node:os", async (importOriginal) => ({
|
|
|
18
18
|
import { runCli, type Command } from "./index.js"
|
|
19
19
|
import { EXIT_USAGE_ERROR, stallDiagnosis, noopText, WIRE_SCHEMA } from "../wire/index.js"
|
|
20
20
|
import { needsOf, runCommand, SelectorUsageError } from "./Program.js"
|
|
21
|
-
import { InMemRepo, makeCapturingCliIo, testLayers, applyEmittedScript } from "../
|
|
21
|
+
import { InMemRepo, makeCapturingCliIo, testLayers, applyEmittedScript } from "../twins/index.js"
|
|
22
22
|
import {
|
|
23
23
|
commitAll,
|
|
24
24
|
DID_NOT_RUN_COMMENT,
|
package/src/cli/Strays.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
2
|
import { describe, expect, it } from "vitest"
|
|
3
3
|
import { EXIT_OK, EXIT_RUNTIME_ERROR, EXIT_STRAYS_FOUND } from "../wire/index.js"
|
|
4
|
-
import { InMemRepo, makeCapturingCliIo } from "../
|
|
4
|
+
import { InMemRepo, makeCapturingCliIo } from "../twins/index.js"
|
|
5
5
|
import { runCli } from "./index.js"
|
|
6
6
|
|
|
7
7
|
const seed = (): InMemRepo => {
|
package/src/edge/Edge.test.ts
CHANGED
package/src/emit/Emit.ts
CHANGED
|
@@ -110,7 +110,7 @@ export const emitScripts = (
|
|
|
110
110
|
optional: assembleScript(optional),
|
|
111
111
|
})
|
|
112
112
|
|
|
113
|
-
/** Exported so `src/
|
|
113
|
+
/** Exported so `src/twins/EmittedScriptRecognizer.ts` can recognize these by exact string comparison. `printf`, not `echo`, avoids shell-specific backslash-escape quirks. */
|
|
114
114
|
export const DID_NOT_RUN_COMMENT =
|
|
115
115
|
"# gtd emitted this and did NOT run it — pipe it into `sh` to land the turn"
|
|
116
116
|
|
package/src/emit/GitScript.ts
CHANGED
|
@@ -130,7 +130,7 @@ export const fetchHistoryRefs = (remote: string): string =>
|
|
|
130
130
|
// No test asserts the two stay byte-identical; a drift between them is a
|
|
131
131
|
// review-time risk this boundary accepts.
|
|
132
132
|
|
|
133
|
-
/** Marks an emitted block as print-only — mirrors `OutcomeScript.ts`'s `OUTCOME_MARKER` verbatim, so `src/
|
|
133
|
+
/** Marks an emitted block as print-only — mirrors `OutcomeScript.ts`'s `OUTCOME_MARKER` verbatim, so `src/twins/EmittedScriptRecognizer.ts` recognizes either path's output identically. */
|
|
134
134
|
const OUTCOME_MARKER = "# gtd: outcome (print-only)"
|
|
135
135
|
|
|
136
136
|
const printfLine = (fmt: string, args: readonly string[]): string =>
|
|
@@ -19,7 +19,7 @@ const printfLine = (fmt: string, args: readonly string[]): string =>
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Marks an emitted block as print-only. Load-bearing beyond documentation:
|
|
22
|
-
* `src/
|
|
22
|
+
* `src/twins/EmittedScriptRecognizer.ts` recognizes an outcome block by
|
|
23
23
|
* this line, and a bare `printf` would be ambiguous with a workflow
|
|
24
24
|
* `script:` command that happens to start the same way.
|
|
25
25
|
*/
|