@pmelab/gtd 36.0.0 → 37.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 +21 -9
- package/claude/hooks/context.ts +66 -0
- package/claude/hooks/drive.ts +3 -0
- package/claude/hooks/register.tsx +69 -6
- package/dist/chunks/{reserved-BAv71I4i.mjs → reserved-zAfoJtR7.mjs} +53 -16
- package/dist/chunks/{tail-kar1zhOb.mjs → tail-BRC4DGdV.mjs} +1 -1
- package/dist/chunks/{wire-D3Rc5r22.mjs → wire-CSsa3Wxd.mjs} +117 -2
- package/dist/gtd.bundle.mjs +498 -56
- package/dist/testkit.mjs +115 -3
- package/dist/wire.mjs +2 -2
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +126 -33
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +174 -44
- package/package.json +1 -1
- package/src/cli/Cli.test.ts +10 -3
- package/src/cli/Cli.ts +76 -1
- package/src/cli/Program.test.ts +1 -0
- package/src/cli/Program.ts +28 -0
- package/src/cli/ToolCommands.ts +3 -0
- package/src/edge/Edge.ts +11 -0
- package/src/flows/runtime.ts +11 -0
- package/src/replay/Replay.ts +51 -2
- package/src/testkit/Layers.ts +1 -0
- package/src/wire/BeatDocument.ts +12 -2
- package/src/wire/BeatStatus.ts +3 -0
- package/src/wire/ContextLimit.fixture.ts +30 -0
- package/src/wire/ContextLimit.test.ts +96 -0
- package/src/wire/ContextLimit.ts +169 -0
- package/src/wire/index.ts +1 -0
- package/src/wire/types.ts +7 -0
- package/src/workflow/Workflow.ts +9 -1
- package/src/workflows/feature/context-limit.feature +37 -0
- package/src/workflows/feature/coverage.ts +8 -2
- package/src/workflows/feature/feature.ts +6 -2
- package/src/workflows/feature/refactor-absorbs.feature +12 -5
- package/src/workflows/feature/refactor-first.feature +11 -2
- package/src/workflows/feature/refactor-lens-models.feature +4 -0
- package/src/workflows/feature/refactor-rebuild.feature +269 -0
- package/src/workflows/feature/refactor-review.feature +45 -1
- package/src/workflows/feature/refactor.ts +9 -3
- package/src/workflows/feature/seam.test.ts +108 -3
- package/src/workflows/feature/seam.ts +120 -19
- package/src/workflows/shared/options.test.ts +11 -2
- package/src/workflows/shared/options.ts +18 -4
- package/src/workflows/shared/steps.ts +1 -0
- package/src/workflows/shared/text.ts +80 -14
package/dist/workflows.mjs
CHANGED
|
@@ -17436,9 +17436,11 @@ ${strays.map((path) => ` - \`${displayPath(path)}\``).join("\n")}
|
|
|
17436
17436
|
- Introduce no new flag read and add no second read of an existing one:
|
|
17437
17437
|
every match of \`${pattern}\` may move, never increase in number`;
|
|
17438
17438
|
const withRules = (rules) => rules === void 0 ? "" : `${rules}\n`;
|
|
17439
|
-
const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false }) => `${stateFileRules}
|
|
17439
|
+
const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false, note = false }) => `${stateFileRules}
|
|
17440
17440
|
- The package to implement is \`${pkg}\`; never delete it
|
|
17441
|
-
${
|
|
17441
|
+
${note ? `- The package's \`## Refactor note\` section names the seams to open and the
|
|
17442
|
+
shape each takes, written from the whole naive pass: build exactly those
|
|
17443
|
+
` : ""}${feedback ? `- The package carries a \`## Review feedback\` section: a human's review of the
|
|
17442
17444
|
last refactor. Act on it, within the rules below
|
|
17443
17445
|
` : ""}${refactorRules(strays, pattern)}
|
|
17444
17446
|
${reference === void 0 ? "" : `- Reference only: \`git show ${reference} -- ${strays.map(quote).join(" ")}\` shows an
|
|
@@ -18057,21 +18059,50 @@ ${criteriaBinding(false)}
|
|
|
18057
18059
|
- Leave \`.gtd/\` untouched
|
|
18058
18060
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
18059
18061
|
${retry === void 0 ? "" : naiveRetry(retry)}`;
|
|
18060
|
-
const
|
|
18062
|
+
const carries = (rebuild) => (rebuild.acceptance?.ids.length ?? 0) > 0;
|
|
18063
|
+
const rebuildLines = ({ note, acceptance }) => `${note === void 0 ? "" : `- The refactor note (\`.gtd/REFACTOR-NOTE.md\`) named the seams the refactor
|
|
18064
|
+
opened; plug the change into them:
|
|
18065
|
+
|
|
18066
|
+
${note.trimEnd().split("\n").map((line) => line === "" ? "" : ` > ${line}`).join("\n")}
|
|
18067
|
+
|
|
18068
|
+
`}${acceptance === void 0 || !carries({ acceptance }) ? "" : `- Carry over the naive pass's acceptance tests for ${acceptance.ids.join(", ")}, the
|
|
18069
|
+
one part of it you take as it is: \`git show ${acceptance.hash} -- ${acceptance.tests.map(quote).join(" ")}\`.
|
|
18070
|
+
Copy each test with its \`[AC-<n>]\` name into the tree as the refactor left
|
|
18071
|
+
it — check a file out of the tip whole only where the refactor did not touch
|
|
18072
|
+
it — changing only what reaches the release flag. A turn whose changed tests
|
|
18073
|
+
leave an ID out is refused, and the suite must be green with them
|
|
18074
|
+
`}`;
|
|
18075
|
+
const refactorNotePrompt = (reference, strays) => `${stateFileRules}
|
|
18076
|
+
- The seam judge ruled the naive pass refactor-first: it is never applied.
|
|
18077
|
+
A behaviour-neutral refactor lands first, then the change is rebuilt on
|
|
18078
|
+
the refactored base. Both turns read the note you write
|
|
18079
|
+
- Read the whole naive pass: \`git show ${reference}\`. It changed these
|
|
18080
|
+
existing files outside the allowlist:
|
|
18081
|
+
${listed(strays)}
|
|
18082
|
+
- Write \`.gtd/REFACTOR-NOTE.md\`: which seams the refactor opens (the existing
|
|
18083
|
+
code to restructure, by path and symbol) and the shape each takes — the
|
|
18084
|
+
signatures and where the new behaviour will plug in. Name only the files
|
|
18085
|
+
listed above: the refactor may change no other path. Never the feature's own
|
|
18086
|
+
behaviour, and no flag read: the refactor adds neither
|
|
18087
|
+
- Change nothing else — a turn that does is refused
|
|
18088
|
+
- Leave everything uncommitted and finish your turn — do not commit
|
|
18089
|
+
`;
|
|
18090
|
+
const afterRefactorLine = (rebuild) => `- A reviewed refactor has just landed and may already carry part of the change,
|
|
18091
|
+
though never a release flag. Build only what is still missing, held to every
|
|
18092
|
+
rule below; if nothing is, ${carries(rebuild) ? "carry over the acceptance tests below and change nothing else" : "change nothing and finish your turn"}
|
|
18093
|
+
`;
|
|
18094
|
+
const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true, rebuild = {}) => `${stateFileRules}
|
|
18061
18095
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18062
18096
|
${criteriaBinding(true)}
|
|
18063
|
-
${afterRefactor === true ?
|
|
18064
|
-
though never a release flag. Build only what is still missing, held to every
|
|
18065
|
-
rule below; if nothing is, change nothing and finish your turn
|
|
18066
|
-
` : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
|
|
18097
|
+
${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
|
|
18067
18098
|
the re-derived requirements instead of rewriting it
|
|
18068
18099
|
` : flagMode ? `- Implement them properly, fitting the codebase's seams and conventions
|
|
18069
18100
|
` : `- Implement them the quickest way that works — the naive version. Do not
|
|
18070
18101
|
restructure, abstract or refactor around the change
|
|
18071
18102
|
`}${reference === void 0 || amend === true ? "" : `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
|
|
18072
18103
|
Read it for ideas; never apply, cherry-pick or check it out — write
|
|
18073
|
-
the change fresh
|
|
18074
|
-
`}${!flagMode ? "- Add no release flag — this project ships without release flags" : tier === "low" ? "- Add no release flag — this change is low risk tier" : amend === true ? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag" : `- Put the new behaviour behind a new release flag, following the
|
|
18104
|
+
the change fresh${carries(rebuild) ? ", its acceptance tests excepted (below)" : ""}
|
|
18105
|
+
`}${rebuildLines(rebuild)}${!flagMode ? "- Add no release flag — this project ships without release flags" : tier === "low" ? "- Add no release flag — this change is low risk tier" : amend === true ? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag" : `- Put the new behaviour behind a new release flag, following the
|
|
18075
18106
|
repository's flag conventions`}
|
|
18076
18107
|
- Write the tests first, then the code that makes them pass. Name each
|
|
18077
18108
|
criterion's ID in the test that covers it (\`[AC-3] …\`): once the suite is
|
|
@@ -18180,6 +18211,7 @@ const ESCALATION = ".gtd/ESCALATION.md";
|
|
|
18180
18211
|
const REQUIREMENTS = ".gtd/REQUIREMENTS.md";
|
|
18181
18212
|
const REVIEW = ".gtd/REVIEW.md";
|
|
18182
18213
|
const REFACTOR = ".gtd/REFACTOR.md";
|
|
18214
|
+
const REFACTOR_NOTE = ".gtd/REFACTOR-NOTE.md";
|
|
18183
18215
|
const QUALITY = ".gtd/QUALITY.md";
|
|
18184
18216
|
const planner = () => env.plannerModel ?? "";
|
|
18185
18217
|
const coder = () => env.coderModel ?? "";
|
|
@@ -37697,7 +37729,11 @@ const ShippedOptions = Struct({
|
|
|
37697
37729
|
testCommand: setting,
|
|
37698
37730
|
plannerModel: setting,
|
|
37699
37731
|
coderModel: setting,
|
|
37700
|
-
flagMode: optionalWith(Boolean$, { exact: true })
|
|
37732
|
+
flagMode: optionalWith(Boolean$, { exact: true }),
|
|
37733
|
+
contextLimit: optionalWith(Struct({
|
|
37734
|
+
soft: optionalWith(Number$, { exact: true }),
|
|
37735
|
+
hard: optionalWith(Number$, { exact: true })
|
|
37736
|
+
}), { exact: true })
|
|
37701
37737
|
});
|
|
37702
37738
|
const over = (base, options) => ({
|
|
37703
37739
|
...base,
|
|
@@ -37709,12 +37745,13 @@ const flagModeOn = () => {
|
|
|
37709
37745
|
if (value !== "true" && value !== "false") refuse(`gtd: flagMode must be "true" or "false", got ${JSON.stringify(value)}`);
|
|
37710
37746
|
return value === "true";
|
|
37711
37747
|
};
|
|
37748
|
+
const limited = (flow, context) => context === void 0 ? flow : () => scope({ context }, flow);
|
|
37712
37749
|
/** A shipped flow as a factory: its options override the settings it declares, each in its own kind. */
|
|
37713
37750
|
const shipped = (flow, doors, base) => defineWorkflow(standardSchemaV1(ShippedOptions, {
|
|
37714
37751
|
onExcessProperty: "error",
|
|
37715
37752
|
errors: "all"
|
|
37716
|
-
}), (options) => ({
|
|
37717
|
-
flow,
|
|
37753
|
+
}), ({ contextLimit, ...options }) => ({
|
|
37754
|
+
flow: limited(flow, contextLimit),
|
|
37718
37755
|
defaults: over(defaults, options),
|
|
37719
37756
|
envDefaults: over(envDefaults, options),
|
|
37720
37757
|
doors,
|
|
@@ -37727,14 +37764,14 @@ const FlagOptions = Struct({
|
|
|
37727
37764
|
const flagShipped = (flow, name) => defineWorkflow(standardSchemaV1(FlagOptions, {
|
|
37728
37765
|
onExcessProperty: "error",
|
|
37729
37766
|
errors: "all"
|
|
37730
|
-
}), (options) => {
|
|
37767
|
+
}), ({ contextLimit, ...options }) => {
|
|
37731
37768
|
const flagDefaults = {
|
|
37732
37769
|
...defaults,
|
|
37733
37770
|
flag: "",
|
|
37734
37771
|
flagRegistry: defaultFlagRegistry
|
|
37735
37772
|
};
|
|
37736
37773
|
return {
|
|
37737
|
-
flow,
|
|
37774
|
+
flow: limited(flow, contextLimit),
|
|
37738
37775
|
defaults: over(flagDefaults, options),
|
|
37739
37776
|
envDefaults: over(envDefaults, options),
|
|
37740
37777
|
doors: { [name]: {
|
|
@@ -38473,7 +38510,8 @@ const plan = (shape, options) => {
|
|
|
38473
38510
|
return {
|
|
38474
38511
|
pattern,
|
|
38475
38512
|
strays,
|
|
38476
|
-
reference: options.reference?.trim() ? options.reference : void 0
|
|
38513
|
+
reference: options.reference?.trim() ? options.reference : void 0,
|
|
38514
|
+
note: options.note?.trim() ? options.note : void 0
|
|
38477
38515
|
};
|
|
38478
38516
|
};
|
|
38479
38517
|
const guardOf = ({ pattern, strays }, entry) => () => {
|
|
@@ -38482,13 +38520,22 @@ const guardOf = ({ pattern, strays }, entry) => () => {
|
|
|
38482
38520
|
};
|
|
38483
38521
|
/** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
|
|
38484
38522
|
const refactorLap = async (p, guard, feedback) => {
|
|
38485
|
-
const { pattern, strays, reference } = p;
|
|
38486
|
-
await run("deriving", deriveScript([
|
|
38487
|
-
|
|
38488
|
-
|
|
38489
|
-
|
|
38490
|
-
|
|
38491
|
-
|
|
38523
|
+
const { pattern, strays, reference, note } = p;
|
|
38524
|
+
await run("deriving", deriveScript([
|
|
38525
|
+
...packageLines(strays, pattern, reference),
|
|
38526
|
+
...note === void 0 ? [] : [
|
|
38527
|
+
"## Refactor note",
|
|
38528
|
+
"",
|
|
38529
|
+
...demoteHeadings(note),
|
|
38530
|
+
""
|
|
38531
|
+
],
|
|
38532
|
+
...feedback === void 0 ? [] : [
|
|
38533
|
+
"## Review feedback",
|
|
38534
|
+
"",
|
|
38535
|
+
...demoteHeadings(feedback),
|
|
38536
|
+
""
|
|
38537
|
+
]
|
|
38538
|
+
]), {
|
|
38492
38539
|
label: "Deriving the refactor package",
|
|
38493
38540
|
file: REFACTOR_PACKAGE
|
|
38494
38541
|
});
|
|
@@ -38497,7 +38544,8 @@ const refactorLap = async (p, guard, feedback) => {
|
|
|
38497
38544
|
strays,
|
|
38498
38545
|
pattern,
|
|
38499
38546
|
reference,
|
|
38500
|
-
feedback: feedback !== void 0
|
|
38547
|
+
feedback: feedback !== void 0,
|
|
38548
|
+
note: note !== void 0
|
|
38501
38549
|
}), {
|
|
38502
38550
|
label: "Refactoring",
|
|
38503
38551
|
model: coder(),
|
|
@@ -38583,13 +38631,16 @@ const COVERED_MIN_P = .7;
|
|
|
38583
38631
|
/** Covering turns before the process rests at `uncovered`; a human edit there refills them. */
|
|
38584
38632
|
const COVERING_RETRIES = 2;
|
|
38585
38633
|
const frozen = () => requireCriteriaFrozen(REQUIREMENTS);
|
|
38586
|
-
const green = () => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
|
|
38634
|
+
const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
|
|
38587
38635
|
label: "Fixing the check",
|
|
38588
38636
|
file: FEEDBACK,
|
|
38589
38637
|
model: coder(),
|
|
38590
38638
|
system: builderSystem()
|
|
38591
38639
|
}), {
|
|
38592
|
-
guard:
|
|
38640
|
+
guard: () => {
|
|
38641
|
+
frozen();
|
|
38642
|
+
guard?.();
|
|
38643
|
+
},
|
|
38593
38644
|
guardRests: false
|
|
38594
38645
|
});
|
|
38595
38646
|
const testsSince = (base) => changesSince(base).filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path)).sort((a, b) => Number(/\bAC-\d/.test(a.after)) - Number(/\bAC-\d/.test(b.after))).map((c) => `=== ${c.path} (${c.status})\n${c.after}`).join("\n");
|
|
@@ -38703,16 +38754,20 @@ const addedLines = (change) => {
|
|
|
38703
38754
|
const before = new Set((change.before ?? "").split("\n"));
|
|
38704
38755
|
return change.after.split("\n").filter((line) => !before.has(line)).join("\n");
|
|
38705
38756
|
};
|
|
38757
|
+
const acceptanceTests = (changes) => changes.filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path));
|
|
38758
|
+
const idsOf = (file) => (criteriaOf(read(file) ?? "") ?? []).map((c) => c.id).filter((id) => id !== "");
|
|
38759
|
+
const unnamed = (ids, tests) => {
|
|
38760
|
+
const lines = tests.map(addedLines);
|
|
38761
|
+
return ids.filter((id) => !lines.some((text) => named(id, text)));
|
|
38762
|
+
};
|
|
38706
38763
|
/**
|
|
38707
38764
|
* Why `tip`'s tests fall short, or `undefined`: a criterion ID of `file` no
|
|
38708
38765
|
* test line of the pass names, or a test importing a file the pass added —
|
|
38709
38766
|
* naive structure a reset and refactor replaces, which the tests must survive.
|
|
38710
38767
|
*/
|
|
38711
38768
|
const acceptanceFault = (file, tip) => {
|
|
38712
|
-
const
|
|
38713
|
-
const
|
|
38714
|
-
const lines = tests.map(addedLines);
|
|
38715
|
-
const untested = ids.filter((id) => !lines.some((text) => named(id, text)));
|
|
38769
|
+
const tests = acceptanceTests(tip.changes);
|
|
38770
|
+
const untested = unnamed(idsOf(file), tests);
|
|
38716
38771
|
if (untested.length > 0) return `acceptance-tests: no test of the naive pass names ${untested.join(", ")} — write a behaviour-level test for each criterion with its ID in the test or scenario name, e.g. "[${untested[0]}] …"`;
|
|
38717
38772
|
const added = tip.changes.filter((c) => c.status === "added" && isCode$1(c.path) && !isTestFile(c.path)).map((c) => c.path);
|
|
38718
38773
|
const reaching = tests.filter((test) => holdsCallSites(test.path)).flatMap((test) => {
|
|
@@ -38721,6 +38776,41 @@ const acceptanceFault = (file, tip) => {
|
|
|
38721
38776
|
});
|
|
38722
38777
|
if (reaching.length > 0) return `acceptance-internals: these tests import files the naive pass added — test through code that existed before it (a public entry point, an existing module's exports):\n${reaching.join("\n")}`;
|
|
38723
38778
|
};
|
|
38779
|
+
/** What a rebuild carries over from the naive pass `tip`: the IDs its tests name, and those test files. */
|
|
38780
|
+
const naiveAcceptance = (tip) => {
|
|
38781
|
+
const ids = idsOf(REQUIREMENTS);
|
|
38782
|
+
const perTest = acceptanceTests(tip.changes).map((c) => {
|
|
38783
|
+
const lines = addedLines(c);
|
|
38784
|
+
return {
|
|
38785
|
+
path: c.path,
|
|
38786
|
+
ids: ids.filter((id) => named(id, lines))
|
|
38787
|
+
};
|
|
38788
|
+
});
|
|
38789
|
+
return {
|
|
38790
|
+
hash: tip.hash,
|
|
38791
|
+
tests: perTest.filter((c) => c.ids.length > 0).map((c) => c.path),
|
|
38792
|
+
ids: ids.filter((id) => perTest.some((c) => c.ids.includes(id)))
|
|
38793
|
+
};
|
|
38794
|
+
};
|
|
38795
|
+
/**
|
|
38796
|
+
* Refuse a rebuild whose test changes since `since` leave out an ID the naive
|
|
38797
|
+
* pass's tests named. A skipped test still counts: the gate reads text, never runs it.
|
|
38798
|
+
*/
|
|
38799
|
+
const requireAcceptance = (acceptance, since) => {
|
|
38800
|
+
const missing = unnamed(acceptance.ids, acceptanceTests(changesSince(since)));
|
|
38801
|
+
if (missing.length === 0) return;
|
|
38802
|
+
refuse(`gtd land: rebuild-acceptance: the rebuild's tests leave out ${missing.join(", ")} — carry over the naive pass's acceptance tests (\`git show ${acceptance.hash} -- ${acceptance.tests.map(quote).join(" ")}\`), each with its ID in the test or scenario name`);
|
|
38803
|
+
};
|
|
38804
|
+
/**
|
|
38805
|
+
* Refuse a fix turn that, on its own, drops an ID the naive pass's tests named
|
|
38806
|
+
* from a test file. Turn-local, so a person's edit at an escalation rest never
|
|
38807
|
+
* wedges later fix turns.
|
|
38808
|
+
*/
|
|
38809
|
+
const requireAcceptanceKept = (acceptance) => {
|
|
38810
|
+
const dropped = acceptance.ids.filter((id) => changes().filter((c) => isCode$1(c.path) && isTestFile(c.path)).some((c) => named(id, c.before ?? "") && !named(id, c.after ?? "")));
|
|
38811
|
+
if (dropped.length === 0) return;
|
|
38812
|
+
refuse(`gtd land: rebuild-acceptance: this turn drops ${dropped.join(", ")} from the tests — the naive pass's acceptance tests stay; fix the code, not them`);
|
|
38813
|
+
};
|
|
38724
38814
|
const restore = (path) => `if git cat-file -e ${quote(`HEAD:${path}`)} 2>/dev/null; then git checkout -q HEAD -- ${quote(path)}; else rm -f -- ${quote(path)}; fi`;
|
|
38725
38815
|
/**
|
|
38726
38816
|
* Run the suite with `tip`'s diff applied, then take it out again; resolves
|
|
@@ -38764,16 +38854,20 @@ function implement(first, tier, amend) {
|
|
|
38764
38854
|
amend
|
|
38765
38855
|
};
|
|
38766
38856
|
return scope("implement", async () => {
|
|
38767
|
-
|
|
38857
|
+
const since = head$1();
|
|
38858
|
+
const acceptance = options.rebuild?.acceptance;
|
|
38859
|
+
const built = options.naive === void 0 || !await applied(options.naive);
|
|
38860
|
+
if (built) await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn(), options.rebuild), {
|
|
38768
38861
|
label: "Implementing",
|
|
38769
38862
|
model: coder(),
|
|
38770
38863
|
system: builderSystem(),
|
|
38771
38864
|
...options.afterRefactor === true ? { allowEmpty: true } : {}
|
|
38772
38865
|
});
|
|
38773
38866
|
requireCriteriaFrozen(REQUIREMENTS);
|
|
38774
|
-
|
|
38867
|
+
if (built && acceptance !== void 0) requireAcceptance(acceptance, since);
|
|
38868
|
+
await green(built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0);
|
|
38775
38869
|
await requireCoverage();
|
|
38776
|
-
await run("closing", removeScript([REQUIREMENTS]), { label: "Closing out the implementation" });
|
|
38870
|
+
await run("closing", removeScript([REQUIREMENTS, REFACTOR_NOTE]), { label: "Closing out the implementation" });
|
|
38777
38871
|
});
|
|
38778
38872
|
}
|
|
38779
38873
|
/** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
|
|
@@ -38823,16 +38917,23 @@ const vetted = async (tip, vetting) => {
|
|
|
38823
38917
|
/** The judge's ruling on a vetted pass: the decision, or the next attempt's verdict. */
|
|
38824
38918
|
const ruled = async (tip, again) => {
|
|
38825
38919
|
const shape = await judgeSeam(tip);
|
|
38826
|
-
if (shape.choice === "land") return {
|
|
38920
|
+
if (shape.choice === "land") return {
|
|
38921
|
+
reference: tip.hash,
|
|
38922
|
+
tip
|
|
38923
|
+
};
|
|
38827
38924
|
if (shape.choice === "refactor-first") return {
|
|
38828
38925
|
reference: tip.hash,
|
|
38926
|
+
tip,
|
|
38829
38927
|
shape
|
|
38830
38928
|
};
|
|
38831
38929
|
const next = await again({
|
|
38832
38930
|
strays: shape.strays,
|
|
38833
38931
|
p: shape.judgment?.answers.shape?.p ?? 0
|
|
38834
38932
|
});
|
|
38835
|
-
return next.kind === "overruled" ? {
|
|
38933
|
+
return next.kind === "overruled" ? {
|
|
38934
|
+
reference: tip.hash,
|
|
38935
|
+
tip
|
|
38936
|
+
} : next;
|
|
38836
38937
|
};
|
|
38837
38938
|
const decide = () => scope("seam", async () => {
|
|
38838
38939
|
const vetting = {
|
|
@@ -38843,13 +38944,29 @@ const decide = () => scope("seam", async () => {
|
|
|
38843
38944
|
for (;;) {
|
|
38844
38945
|
await naivePass(retry);
|
|
38845
38946
|
const tip = kept("naive");
|
|
38846
|
-
const outcome = await vetted(tip, vetting) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
|
|
38947
|
+
const outcome = await vetted(tip, vetting) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
|
|
38948
|
+
reference: tip.hash,
|
|
38949
|
+
tip
|
|
38950
|
+
});
|
|
38847
38951
|
if (!("kind" in outcome)) return outcome;
|
|
38848
38952
|
retry = outcome.kind === "retry" ? outcome.retry : void 0;
|
|
38849
38953
|
}
|
|
38850
38954
|
});
|
|
38955
|
+
/** Write the refactor note from the whole naive pass; a turn that changes anything else is refused. */
|
|
38956
|
+
const refactorNote = async (reference, strays) => {
|
|
38957
|
+
await agent("note", refactorNotePrompt(reference, strays), {
|
|
38958
|
+
label: "Writing the refactor note",
|
|
38959
|
+
file: REFACTOR_NOTE,
|
|
38960
|
+
model: coder(),
|
|
38961
|
+
system: builderSystem()
|
|
38962
|
+
});
|
|
38963
|
+
const others = changes().map((c) => c.path).filter((path) => path !== REFACTOR_NOTE);
|
|
38964
|
+
const note = read(".gtd/REFACTOR-NOTE.md") ?? "";
|
|
38965
|
+
if (others.length > 0 || note.trim() === "") refuse(`gtd land: refactor-note: write ${REFACTOR_NOTE} and change nothing else${others.length > 0 ? ` — revert these:\n${others.map((path) => ` - ${path}`).join("\n")}` : ""}`);
|
|
38966
|
+
return note;
|
|
38967
|
+
};
|
|
38851
38968
|
const seamBuild = async (base) => {
|
|
38852
|
-
const { reference, shape } = await decide();
|
|
38969
|
+
const { reference, tip, shape } = await decide();
|
|
38853
38970
|
if (shape === void 0) return {
|
|
38854
38971
|
reference,
|
|
38855
38972
|
base,
|
|
@@ -38863,18 +38980,29 @@ const seamBuild = async (base) => {
|
|
|
38863
38980
|
}));
|
|
38864
38981
|
return {
|
|
38865
38982
|
reference,
|
|
38866
|
-
base
|
|
38983
|
+
base,
|
|
38984
|
+
rebuild: { acceptance: naiveAcceptance(tip) }
|
|
38867
38985
|
};
|
|
38868
38986
|
}
|
|
38869
|
-
|
|
38987
|
+
const strays = refactorStrays(shape);
|
|
38988
|
+
if (strays.length === 0) return {
|
|
38870
38989
|
reference,
|
|
38871
|
-
base
|
|
38990
|
+
base,
|
|
38991
|
+
rebuild: { acceptance: naiveAcceptance(tip) }
|
|
38872
38992
|
};
|
|
38873
|
-
const
|
|
38993
|
+
const note = await scope("seam", () => refactorNote(reference, strays));
|
|
38994
|
+
const refactored = await slice("refactor", () => reviewedRefactor(shape, {
|
|
38995
|
+
reference,
|
|
38996
|
+
note
|
|
38997
|
+
}));
|
|
38874
38998
|
return {
|
|
38875
38999
|
reference,
|
|
38876
39000
|
base: head$1(),
|
|
38877
|
-
refactored
|
|
39001
|
+
refactored,
|
|
39002
|
+
rebuild: {
|
|
39003
|
+
note,
|
|
39004
|
+
acceptance: naiveAcceptance(tip)
|
|
39005
|
+
}
|
|
38878
39006
|
};
|
|
38879
39007
|
};
|
|
38880
39008
|
//#endregion
|
|
@@ -38893,9 +39021,11 @@ const buildFeature = async (firstBase) => {
|
|
|
38893
39021
|
reference: seam.reference,
|
|
38894
39022
|
tier,
|
|
38895
39023
|
afterRefactor: seam.refactored,
|
|
38896
|
-
naive: seam.land === true ? seam.reference : void 0
|
|
39024
|
+
naive: seam.land === true ? seam.reference : void 0,
|
|
39025
|
+
rebuild: seam.rebuild
|
|
38897
39026
|
});
|
|
38898
|
-
const
|
|
39027
|
+
const carried = seam.rebuild?.acceptance?.tests ?? [];
|
|
39028
|
+
const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path) && !carried.includes(c.path)) ? firstBase : seam.base;
|
|
38899
39029
|
await tail(settleQuality, {
|
|
38900
39030
|
green: true,
|
|
38901
39031
|
lensBase: base
|
|
@@ -39138,4 +39268,4 @@ var workflows_default = feature;
|
|
|
39138
39268
|
*/
|
|
39139
39269
|
const doors = {};
|
|
39140
39270
|
//#endregion
|
|
39141
|
-
export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, criteriaOf, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireCriteria, requireCriteriaFrozen, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, statedCriteria, steering, summary, tail, threads, triage, unwind };
|
|
39271
|
+
export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, criteriaOf, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, statedCriteria, steering, summary, tail, threads, triage, unwind };
|
package/package.json
CHANGED
package/src/cli/Cli.test.ts
CHANGED
|
@@ -1106,6 +1106,7 @@ describe("standaloneKinds / needsOf", () => {
|
|
|
1106
1106
|
"uncheck",
|
|
1107
1107
|
"install",
|
|
1108
1108
|
"loop",
|
|
1109
|
+
"contextHook",
|
|
1109
1110
|
"judgeRun",
|
|
1110
1111
|
"strays",
|
|
1111
1112
|
])
|
|
@@ -1117,6 +1118,7 @@ describe("standaloneKinds / needsOf", () => {
|
|
|
1117
1118
|
expect(needsOf("uncheck")).toBe("fs")
|
|
1118
1119
|
expect(needsOf("install")).toBe("none")
|
|
1119
1120
|
expect(needsOf("loop")).toBe("none")
|
|
1121
|
+
expect(needsOf("contextHook")).toBe("none")
|
|
1120
1122
|
expect(needsOf("judgeRun")).toBe("none")
|
|
1121
1123
|
expect(needsOf("strays")).toBe("none")
|
|
1122
1124
|
for (const kind of [
|
|
@@ -1202,19 +1204,24 @@ describe("renderHelp", () => {
|
|
|
1202
1204
|
|
|
1203
1205
|
it("README.md and COMPATIBILITY.md list exactly the commands", () => {
|
|
1204
1206
|
const commands = new Set(
|
|
1205
|
-
[...renderHelp().matchAll(/^ {2}([a-z]+(?: [a-z]+)?)(?= )/gm)].map(
|
|
1207
|
+
[...renderHelp().matchAll(/^ {2}([a-z]+(?:-[a-z]+)?(?: [a-z]+)?)(?= )/gm)].map(
|
|
1208
|
+
([, name]) => name,
|
|
1209
|
+
),
|
|
1206
1210
|
)
|
|
1207
1211
|
expect(commands).toContain("judge answer")
|
|
1212
|
+
expect(commands).toContain("context-hook")
|
|
1208
1213
|
const listed = (file: string, list: RegExp): Set<string> => {
|
|
1209
1214
|
const doc = readFileSync(resolve(import.meta.dirname, file), "utf8")
|
|
1210
1215
|
const match = doc.match(list)
|
|
1211
1216
|
expect(match, file).not.toBeNull()
|
|
1212
|
-
return new Set(
|
|
1217
|
+
return new Set(
|
|
1218
|
+
[...match![1]!.matchAll(/`([a-z]+(?:-[a-z]+)?(?: [a-z]+)?)`/g)].map(([, name]) => name!),
|
|
1219
|
+
)
|
|
1213
1220
|
}
|
|
1214
1221
|
expect(
|
|
1215
1222
|
listed("../../README.md", /The `gtd` bin's commands — ([\s\S]*?) — are\s+listed/),
|
|
1216
1223
|
).toEqual(commands)
|
|
1217
|
-
expect(listed("../../COMPATIBILITY.md", /every command \(([\s\S]*?)\)
|
|
1224
|
+
expect(listed("../../COMPATIBILITY.md", /every command \(([\s\S]*?)\),\s+option/)).toEqual(
|
|
1218
1225
|
commands,
|
|
1219
1226
|
)
|
|
1220
1227
|
})
|
package/src/cli/Cli.ts
CHANGED
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
type JsonMode,
|
|
36
36
|
} from "./Program.js"
|
|
37
37
|
import type { ToolCommands } from "./ToolCommands.js"
|
|
38
|
-
import { EXIT_RUNTIME_ERROR, EXIT_USAGE_ERROR } from "../wire/index.js"
|
|
38
|
+
import { EXIT_RUNTIME_ERROR, EXIT_USAGE_ERROR, parseContextWindow } from "../wire/index.js"
|
|
39
39
|
|
|
40
40
|
export type { Command, CommandRequirements } from "./Program.js"
|
|
41
41
|
export { needsOf, standaloneKinds } from "./Program.js"
|
|
@@ -90,6 +90,13 @@ const nonNegativeNumber = (raw: string, flag: string): Either.Either<number, str
|
|
|
90
90
|
return Either.right(n)
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
const share = (raw: string, flag: string): Either.Either<number, string> => {
|
|
94
|
+
const n = Number(raw)
|
|
95
|
+
return raw.trim() !== "" && n > 0 && n <= 1
|
|
96
|
+
? Either.right(n)
|
|
97
|
+
: Either.left(`gtd: ${flag} must be a share of the context window in (0, 1] — got "${raw}"`)
|
|
98
|
+
}
|
|
99
|
+
|
|
93
100
|
const FLAGS: readonly FlagRow[] = [
|
|
94
101
|
{
|
|
95
102
|
name: "--json",
|
|
@@ -245,6 +252,41 @@ const FLAGS: readonly FlagRow[] = [
|
|
|
245
252
|
"GTD_JUDGE_MODEL, then judge.json, then sonnet",
|
|
246
253
|
],
|
|
247
254
|
},
|
|
255
|
+
{
|
|
256
|
+
name: "--soft",
|
|
257
|
+
arity: 1,
|
|
258
|
+
repeatable: false,
|
|
259
|
+
scope: (kind) => kind === "contextHook",
|
|
260
|
+
decode: ([raw]) => share(raw ?? "", "--soft"),
|
|
261
|
+
scopeError: "gtd: --soft is only valid for `gtd context-hook`",
|
|
262
|
+
valueHint: "<share>",
|
|
263
|
+
help: ["(gtd context-hook only) the soft limit; default 0.6"],
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: "--hard",
|
|
267
|
+
arity: 1,
|
|
268
|
+
repeatable: false,
|
|
269
|
+
scope: (kind) => kind === "contextHook",
|
|
270
|
+
decode: ([raw]) => share(raw ?? "", "--hard"),
|
|
271
|
+
scopeError: "gtd: --hard is only valid for `gtd context-hook`",
|
|
272
|
+
valueHint: "<share>",
|
|
273
|
+
help: ["(gtd context-hook only) the hard limit; default 0.7"],
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: "--window",
|
|
277
|
+
arity: 1,
|
|
278
|
+
repeatable: false,
|
|
279
|
+
scope: (kind) => kind === "contextHook",
|
|
280
|
+
decode: ([raw]) => {
|
|
281
|
+
const window = parseContextWindow(raw)
|
|
282
|
+
return window !== undefined
|
|
283
|
+
? Either.right(window)
|
|
284
|
+
: Either.left(`gtd: --window must be a positive integer — got "${raw ?? ""}"`)
|
|
285
|
+
},
|
|
286
|
+
scopeError: "gtd: --window is only valid for `gtd context-hook`",
|
|
287
|
+
valueHint: "<tokens>",
|
|
288
|
+
help: ["(gtd context-hook only) the model's context window in", "tokens; default 200000"],
|
|
289
|
+
},
|
|
248
290
|
{
|
|
249
291
|
name: "--slice",
|
|
250
292
|
arity: 1,
|
|
@@ -593,6 +635,22 @@ const COMMAND_ROWS: readonly CommandRow[] = [
|
|
|
593
635
|
"~/.config/gtd/loop.json); see docs/driver.md",
|
|
594
636
|
],
|
|
595
637
|
},
|
|
638
|
+
{
|
|
639
|
+
token: "context-hook",
|
|
640
|
+
kind: "contextHook",
|
|
641
|
+
arity: "none",
|
|
642
|
+
details: [
|
|
643
|
+
"A Claude Code PostToolUse and PreCompact hook that stops an",
|
|
644
|
+
"agent turn at a share of its context window. Reads the",
|
|
645
|
+
"hook's input on stdin and the context the transcript's",
|
|
646
|
+
"latest assistant message used. Past --soft it blocks with a",
|
|
647
|
+
"request to end the turn with a handoff note; past --hard,",
|
|
648
|
+
"or at PreCompact, it stops the turn with a stopReason",
|
|
649
|
+
'starting "gtd: context limit". Prints nothing otherwise,',
|
|
650
|
+
"and for a subagent's tool call. Always exits 0 on valid",
|
|
651
|
+
"input; see docs/driver.md",
|
|
652
|
+
],
|
|
653
|
+
},
|
|
596
654
|
{
|
|
597
655
|
token: "summary",
|
|
598
656
|
kind: "summary",
|
|
@@ -1182,6 +1240,9 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1182
1240
|
readonly "--provider"?: "fixed" | "jev" | "llm"
|
|
1183
1241
|
readonly "--answers"?: string
|
|
1184
1242
|
readonly "--slice"?: string
|
|
1243
|
+
readonly "--soft"?: number
|
|
1244
|
+
readonly "--hard"?: number
|
|
1245
|
+
readonly "--window"?: number
|
|
1185
1246
|
}
|
|
1186
1247
|
|
|
1187
1248
|
// `present.has("--json")` alone can't distinguish bare `--json` (no value
|
|
@@ -1285,6 +1346,20 @@ export const parseArgv = (argv: readonly string[]): CliPlan => {
|
|
|
1285
1346
|
}
|
|
1286
1347
|
}
|
|
1287
1348
|
|
|
1349
|
+
if (kind === "contextHook") {
|
|
1350
|
+
return {
|
|
1351
|
+
kind: "command",
|
|
1352
|
+
command: {
|
|
1353
|
+
kind: "contextHook",
|
|
1354
|
+
...(bag["--soft"] !== undefined ? { soft: bag["--soft"] } : {}),
|
|
1355
|
+
...(bag["--hard"] !== undefined ? { hard: bag["--hard"] } : {}),
|
|
1356
|
+
...(bag["--window"] !== undefined ? { window: bag["--window"] } : {}),
|
|
1357
|
+
},
|
|
1358
|
+
json,
|
|
1359
|
+
verbose,
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1288
1363
|
if (kind === "summary") {
|
|
1289
1364
|
return {
|
|
1290
1365
|
kind: "command",
|
package/src/cli/Program.test.ts
CHANGED
|
@@ -1686,6 +1686,7 @@ describe("runCommand — refuses in a repository with no commits", () => {
|
|
|
1686
1686
|
uncheck: { kind: "uncheck", file: ".gtd/REVIEW.md" },
|
|
1687
1687
|
install: { kind: "install" },
|
|
1688
1688
|
loop: { kind: "loop" },
|
|
1689
|
+
contextHook: { kind: "contextHook" },
|
|
1689
1690
|
summary: { kind: "summary" },
|
|
1690
1691
|
base: { kind: "base" },
|
|
1691
1692
|
judge: { kind: "judge" },
|
package/src/cli/Program.ts
CHANGED
|
@@ -162,6 +162,12 @@ export type Command =
|
|
|
162
162
|
| { readonly kind: "uncheck"; readonly file: string }
|
|
163
163
|
| { readonly kind: "install" }
|
|
164
164
|
| { readonly kind: "loop" }
|
|
165
|
+
| {
|
|
166
|
+
readonly kind: "contextHook"
|
|
167
|
+
readonly soft?: number
|
|
168
|
+
readonly hard?: number
|
|
169
|
+
readonly window?: number
|
|
170
|
+
}
|
|
165
171
|
| { readonly kind: "summary"; readonly slice?: string }
|
|
166
172
|
| { readonly kind: "base" }
|
|
167
173
|
| { readonly kind: "judge" }
|
|
@@ -434,6 +440,24 @@ const readStdin = (): Effect.Effect<string, Error> =>
|
|
|
434
440
|
catch: (e) => (e instanceof Error ? e : new Error(String(e))),
|
|
435
441
|
})
|
|
436
442
|
|
|
443
|
+
/** Stdout is written once: a hook reads one JSON document, or none. */
|
|
444
|
+
const runContextHookCommand = (
|
|
445
|
+
command: Extract<Command, { kind: "contextHook" }>,
|
|
446
|
+
out: ArtifactOut,
|
|
447
|
+
): Effect.Effect<void, Error, ToolCommands> =>
|
|
448
|
+
Effect.gen(function* () {
|
|
449
|
+
const tools = yield* ToolCommands
|
|
450
|
+
const input = yield* readStdin()
|
|
451
|
+
out.write(
|
|
452
|
+
yield* tools.contextHook({
|
|
453
|
+
input,
|
|
454
|
+
...(command.soft !== undefined ? { soft: command.soft } : {}),
|
|
455
|
+
...(command.hard !== undefined ? { hard: command.hard } : {}),
|
|
456
|
+
...(command.window !== undefined ? { window: command.window } : {}),
|
|
457
|
+
}),
|
|
458
|
+
)
|
|
459
|
+
})
|
|
460
|
+
|
|
437
461
|
/** Stdout is written once, after the answerer resolves — a rejection leaves it empty. */
|
|
438
462
|
const runJudgeRunCommand = (
|
|
439
463
|
command: Extract<Command, { kind: "judgeRun" }>,
|
|
@@ -1189,6 +1213,7 @@ export const needsOf = (kind: Command["kind"]): Needs => {
|
|
|
1189
1213
|
case "lsp":
|
|
1190
1214
|
case "install":
|
|
1191
1215
|
case "loop":
|
|
1216
|
+
case "contextHook":
|
|
1192
1217
|
case "judgeRun":
|
|
1193
1218
|
case "strays":
|
|
1194
1219
|
return "none"
|
|
@@ -1207,6 +1232,7 @@ export const standaloneKinds = (): readonly Command["kind"][] => [
|
|
|
1207
1232
|
"uncheck",
|
|
1208
1233
|
"install",
|
|
1209
1234
|
"loop",
|
|
1235
|
+
"contextHook",
|
|
1210
1236
|
"judgeRun",
|
|
1211
1237
|
"strays",
|
|
1212
1238
|
]
|
|
@@ -1278,6 +1304,8 @@ const dispatchVoidCommand = (
|
|
|
1278
1304
|
return runInstallCommand(out)
|
|
1279
1305
|
case "loop":
|
|
1280
1306
|
return runLoopCommand()
|
|
1307
|
+
case "contextHook":
|
|
1308
|
+
return runContextHookCommand(command, out)
|
|
1281
1309
|
case "summary":
|
|
1282
1310
|
return runSummaryCommand(out, command.slice)
|
|
1283
1311
|
case "base":
|
package/src/cli/ToolCommands.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Context } from "effect"
|
|
2
2
|
import type { Effect } from "effect"
|
|
3
3
|
import type { UiSettings } from "../flows/index.js"
|
|
4
|
+
import type { ContextHookRequest } from "../wire/index.js"
|
|
4
5
|
|
|
5
6
|
/** How to run THIS gtd — a driver (the loop, the LSP) never reads a different version's state than the one it ships with. */
|
|
6
7
|
export type SelfGtd = readonly [string, ...string[]]
|
|
@@ -38,6 +39,8 @@ export class ToolCommands extends Context.Tag("ToolCommands")<
|
|
|
38
39
|
) => Effect.Effect<void, Error>
|
|
39
40
|
readonly lsp: (gtd: SelfGtd) => Effect.Effect<void, Error>
|
|
40
41
|
readonly loop: (gtd: SelfGtd) => Effect.Effect<void, Error>
|
|
42
|
+
/** The hook's stdout: a JSON document, or empty to let the turn go on. */
|
|
43
|
+
readonly contextHook: (request: ContextHookRequest) => Effect.Effect<string, Error>
|
|
41
44
|
readonly judge: (request: JudgeRequest) => Promise<string>
|
|
42
45
|
}
|
|
43
46
|
>() {}
|