gsd-pi 2.59.0-dev.3de3832 → 2.59.0-dev.d77b3dd
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/dist/resources/extensions/gsd/auto/phases.js +54 -1
- package/dist/resources/extensions/gsd/auto-model-selection.js +8 -3
- package/dist/resources/extensions/gsd/auto-post-unit.js +40 -1
- package/dist/resources/extensions/gsd/auto-prompts.js +13 -0
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +70 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +51 -5
- package/dist/resources/extensions/gsd/captures.js +54 -1
- package/dist/resources/extensions/gsd/complexity-classifier.js +1 -1
- package/dist/resources/extensions/gsd/context-masker.js +68 -0
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +7 -0
- package/dist/resources/extensions/gsd/gsd-db.js +2 -2
- package/dist/resources/extensions/gsd/model-router.js +123 -4
- package/dist/resources/extensions/gsd/phase-anchor.js +56 -0
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +46 -0
- package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -0
- package/dist/resources/extensions/gsd/prompts/rethink.md +7 -0
- package/dist/resources/extensions/gsd/prompts/triage-captures.md +6 -1
- package/dist/resources/extensions/gsd/rethink.js +5 -2
- package/dist/resources/extensions/gsd/state.js +1 -1
- package/dist/resources/extensions/gsd/status-guards.js +4 -3
- package/dist/resources/extensions/gsd/triage-resolution.js +128 -1
- package/dist/resources/extensions/gsd/triage-ui.js +12 -3
- package/dist/resources/skills/btw/SKILL.md +42 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +20 -20
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +20 -20
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/src/resources/extensions/gsd/auto/phases.ts +60 -1
- package/src/resources/extensions/gsd/auto-model-selection.ts +12 -3
- package/src/resources/extensions/gsd/auto-post-unit.ts +48 -1
- package/src/resources/extensions/gsd/auto-prompts.ts +17 -0
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +78 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +53 -4
- package/src/resources/extensions/gsd/captures.ts +71 -2
- package/src/resources/extensions/gsd/complexity-classifier.ts +1 -1
- package/src/resources/extensions/gsd/context-masker.ts +74 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +7 -0
- package/src/resources/extensions/gsd/gsd-db.ts +2 -2
- package/src/resources/extensions/gsd/model-router.ts +171 -8
- package/src/resources/extensions/gsd/phase-anchor.ts +71 -0
- package/src/resources/extensions/gsd/preferences-types.ts +9 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +38 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -0
- package/src/resources/extensions/gsd/prompts/rethink.md +7 -0
- package/src/resources/extensions/gsd/prompts/triage-captures.md +6 -1
- package/src/resources/extensions/gsd/rethink.ts +5 -2
- package/src/resources/extensions/gsd/state.ts +1 -1
- package/src/resources/extensions/gsd/status-guards.ts +4 -3
- package/src/resources/extensions/gsd/tests/context-masker.test.ts +122 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +87 -1
- package/src/resources/extensions/gsd/tests/phase-anchor.test.ts +83 -0
- package/src/resources/extensions/gsd/tests/status-guards.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/stop-backtrack.test.ts +216 -0
- package/src/resources/extensions/gsd/tests/tool-naming.test.ts +1 -1
- package/src/resources/extensions/gsd/triage-resolution.ts +144 -1
- package/src/resources/extensions/gsd/triage-ui.ts +12 -3
- package/src/resources/skills/btw/SKILL.md +42 -0
- /package/dist/web/standalone/.next/static/{Y_HG7cJVptjBpkVSQQiFi → t_cBZAENjaOJIRST3dw08}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{Y_HG7cJVptjBpkVSQQiFi → t_cBZAENjaOJIRST3dw08}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for stop/backtrack capture classifications and milestone regression (#3487).
|
|
3
|
+
*
|
|
4
|
+
* Tests:
|
|
5
|
+
* - "stop" and "backtrack" are valid classification types
|
|
6
|
+
* - loadStopCaptures returns unexecuted stop+backtrack captures
|
|
7
|
+
* - loadBacktrackCaptures returns only backtrack captures
|
|
8
|
+
* - revertExecutorResolvedCaptures reverts silenced captures
|
|
9
|
+
* - executeBacktrack writes trigger and regression markers
|
|
10
|
+
* - readBacktrackTrigger parses trigger file
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { mkdirSync, readFileSync, writeFileSync, rmSync, existsSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { isClosedStatus } from "../status-guards.ts";
|
|
19
|
+
import {
|
|
20
|
+
appendCapture,
|
|
21
|
+
loadAllCaptures,
|
|
22
|
+
loadStopCaptures,
|
|
23
|
+
loadBacktrackCaptures,
|
|
24
|
+
markCaptureResolved,
|
|
25
|
+
revertExecutorResolvedCaptures,
|
|
26
|
+
hasPendingCaptures,
|
|
27
|
+
} from "../captures.ts";
|
|
28
|
+
import {
|
|
29
|
+
executeBacktrack,
|
|
30
|
+
readBacktrackTrigger,
|
|
31
|
+
} from "../triage-resolution.ts";
|
|
32
|
+
|
|
33
|
+
function makeTempDir(prefix: string): string {
|
|
34
|
+
const dir = join(
|
|
35
|
+
tmpdir(),
|
|
36
|
+
`${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
37
|
+
);
|
|
38
|
+
mkdirSync(dir, { recursive: true });
|
|
39
|
+
return dir;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function setupGsdDir(tmp: string): void {
|
|
43
|
+
mkdirSync(join(tmp, ".gsd"), { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Classification Types ─────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
test("stop is a valid classification", () => {
|
|
49
|
+
const tmp = makeTempDir("stop-class");
|
|
50
|
+
setupGsdDir(tmp);
|
|
51
|
+
const id = appendCapture(tmp, "stop running immediately");
|
|
52
|
+
markCaptureResolved(tmp, id, "stop", "Halt auto-mode", "User said stop", "M005");
|
|
53
|
+
const all = loadAllCaptures(tmp);
|
|
54
|
+
const cap = all.find(c => c.id === id);
|
|
55
|
+
assert.equal(cap?.classification, "stop");
|
|
56
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("backtrack is a valid classification", () => {
|
|
60
|
+
const tmp = makeTempDir("bt-class");
|
|
61
|
+
setupGsdDir(tmp);
|
|
62
|
+
const id = appendCapture(tmp, "restart from M003");
|
|
63
|
+
markCaptureResolved(tmp, id, "backtrack", "Backtrack to M003", "User wants to restart", "M005");
|
|
64
|
+
const all = loadAllCaptures(tmp);
|
|
65
|
+
const cap = all.find(c => c.id === id);
|
|
66
|
+
assert.equal(cap?.classification, "backtrack");
|
|
67
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ─── loadStopCaptures ─────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
test("loadStopCaptures returns unexecuted stop and backtrack captures", () => {
|
|
73
|
+
const tmp = makeTempDir("load-stop");
|
|
74
|
+
setupGsdDir(tmp);
|
|
75
|
+
const stopId = appendCapture(tmp, "halt execution");
|
|
76
|
+
const btId = appendCapture(tmp, "go back to M003");
|
|
77
|
+
const noteId = appendCapture(tmp, "just a note");
|
|
78
|
+
markCaptureResolved(tmp, stopId, "stop", "Halt", "User stop", "M005");
|
|
79
|
+
markCaptureResolved(tmp, btId, "backtrack", "Backtrack to M003", "User backtrack", "M005");
|
|
80
|
+
markCaptureResolved(tmp, noteId, "note", "Info only", "Not actionable", "M005");
|
|
81
|
+
|
|
82
|
+
const stops = loadStopCaptures(tmp);
|
|
83
|
+
assert.equal(stops.length, 2);
|
|
84
|
+
assert.ok(stops.some(c => c.classification === "stop"));
|
|
85
|
+
assert.ok(stops.some(c => c.classification === "backtrack"));
|
|
86
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("loadBacktrackCaptures returns only backtrack captures", () => {
|
|
90
|
+
const tmp = makeTempDir("load-bt");
|
|
91
|
+
setupGsdDir(tmp);
|
|
92
|
+
const stopId = appendCapture(tmp, "halt execution");
|
|
93
|
+
const btId = appendCapture(tmp, "go back to M003");
|
|
94
|
+
markCaptureResolved(tmp, stopId, "stop", "Halt", "User stop", "M005");
|
|
95
|
+
markCaptureResolved(tmp, btId, "backtrack", "Backtrack to M003", "User backtrack", "M005");
|
|
96
|
+
|
|
97
|
+
const bts = loadBacktrackCaptures(tmp);
|
|
98
|
+
assert.equal(bts.length, 1);
|
|
99
|
+
assert.equal(bts[0].classification, "backtrack");
|
|
100
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ─── revertExecutorResolvedCaptures ───────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
test("revertExecutorResolvedCaptures reverts captures resolved without classification", () => {
|
|
106
|
+
const tmp = makeTempDir("revert-exec");
|
|
107
|
+
setupGsdDir(tmp);
|
|
108
|
+
const id = appendCapture(tmp, "stop everything");
|
|
109
|
+
|
|
110
|
+
// Simulate an executor writing Status: resolved directly (no classification)
|
|
111
|
+
const capPath = join(tmp, ".gsd", "CAPTURES.md");
|
|
112
|
+
let content = readFileSync(capPath, "utf-8");
|
|
113
|
+
content = content.replace("**Status:** pending", "**Status:** resolved");
|
|
114
|
+
writeFileSync(capPath, content, "utf-8");
|
|
115
|
+
|
|
116
|
+
// Verify it's now "resolved" without classification
|
|
117
|
+
assert.equal(hasPendingCaptures(tmp), false);
|
|
118
|
+
|
|
119
|
+
// Revert should detect and fix it
|
|
120
|
+
const reverted = revertExecutorResolvedCaptures(tmp);
|
|
121
|
+
assert.equal(reverted, 1);
|
|
122
|
+
|
|
123
|
+
// Should be pending again
|
|
124
|
+
assert.equal(hasPendingCaptures(tmp), true);
|
|
125
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("revertExecutorResolvedCaptures does NOT revert properly triaged captures", () => {
|
|
129
|
+
const tmp = makeTempDir("revert-skip");
|
|
130
|
+
setupGsdDir(tmp);
|
|
131
|
+
const id = appendCapture(tmp, "restart from M003");
|
|
132
|
+
markCaptureResolved(tmp, id, "backtrack", "Backtrack to M003", "User wants restart", "M005");
|
|
133
|
+
|
|
134
|
+
// This capture was properly triaged — should NOT be reverted
|
|
135
|
+
const reverted = revertExecutorResolvedCaptures(tmp);
|
|
136
|
+
assert.equal(reverted, 0);
|
|
137
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ─── executeBacktrack ─────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
test("executeBacktrack writes trigger and regression markers", () => {
|
|
143
|
+
const tmp = makeTempDir("exec-bt");
|
|
144
|
+
setupGsdDir(tmp);
|
|
145
|
+
|
|
146
|
+
// Create target milestone directory
|
|
147
|
+
mkdirSync(join(tmp, ".gsd", "milestones", "M003"), { recursive: true });
|
|
148
|
+
|
|
149
|
+
const targetMid = executeBacktrack(tmp, "M005", {
|
|
150
|
+
id: "CAP-test123",
|
|
151
|
+
text: "restart from M003 — milestones after 2 failed",
|
|
152
|
+
timestamp: new Date().toISOString(),
|
|
153
|
+
status: "resolved",
|
|
154
|
+
classification: "backtrack",
|
|
155
|
+
resolution: "Backtrack to M003",
|
|
156
|
+
rationale: "User directive",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
assert.equal(targetMid, "M003");
|
|
160
|
+
|
|
161
|
+
// Check trigger file exists
|
|
162
|
+
const triggerPath = join(tmp, ".gsd", "BACKTRACK-TRIGGER.md");
|
|
163
|
+
assert.ok(existsSync(triggerPath));
|
|
164
|
+
const triggerContent = readFileSync(triggerPath, "utf-8");
|
|
165
|
+
assert.ok(triggerContent.includes("M005"));
|
|
166
|
+
assert.ok(triggerContent.includes("M003"));
|
|
167
|
+
|
|
168
|
+
// Check regression marker exists on target milestone
|
|
169
|
+
const regressionPath = join(tmp, ".gsd", "milestones", "M003", "M003-REGRESSION.md");
|
|
170
|
+
assert.ok(existsSync(regressionPath));
|
|
171
|
+
const regressionContent = readFileSync(regressionPath, "utf-8");
|
|
172
|
+
assert.ok(regressionContent.includes("M005"));
|
|
173
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ─── readBacktrackTrigger ─────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
test("readBacktrackTrigger parses trigger file", () => {
|
|
179
|
+
const tmp = makeTempDir("read-bt");
|
|
180
|
+
setupGsdDir(tmp);
|
|
181
|
+
mkdirSync(join(tmp, ".gsd", "milestones", "M003"), { recursive: true });
|
|
182
|
+
|
|
183
|
+
executeBacktrack(tmp, "M005", {
|
|
184
|
+
id: "CAP-abc",
|
|
185
|
+
text: "go back to M003",
|
|
186
|
+
timestamp: new Date().toISOString(),
|
|
187
|
+
status: "resolved",
|
|
188
|
+
classification: "backtrack",
|
|
189
|
+
resolution: "Backtrack to M003",
|
|
190
|
+
rationale: "Regression",
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const trigger = readBacktrackTrigger(tmp);
|
|
194
|
+
assert.ok(trigger);
|
|
195
|
+
assert.equal(trigger.target, "M003");
|
|
196
|
+
assert.equal(trigger.from, "M005");
|
|
197
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("readBacktrackTrigger returns null when no trigger exists", () => {
|
|
201
|
+
const tmp = makeTempDir("no-bt");
|
|
202
|
+
setupGsdDir(tmp);
|
|
203
|
+
const trigger = readBacktrackTrigger(tmp);
|
|
204
|
+
assert.equal(trigger, null);
|
|
205
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ─── Slice Skip Status (#3477) ──────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
test("isClosedStatus treats 'skipped' as closed", () => {
|
|
211
|
+
assert.equal(isClosedStatus("skipped"), true);
|
|
212
|
+
assert.equal(isClosedStatus("complete"), true);
|
|
213
|
+
assert.equal(isClosedStatus("done"), true);
|
|
214
|
+
assert.equal(isClosedStatus("pending"), false);
|
|
215
|
+
assert.equal(isClosedStatus("active"), false);
|
|
216
|
+
});
|
|
@@ -45,7 +45,7 @@ console.log('\n── Tool naming: registration count ──');
|
|
|
45
45
|
const pi = makeMockPi();
|
|
46
46
|
registerDbTools(pi);
|
|
47
47
|
|
|
48
|
-
assert.deepStrictEqual(pi.tools.length,
|
|
48
|
+
assert.deepStrictEqual(pi.tools.length, 30, 'Should register exactly 30 tools (14 canonical + 14 aliases + 1 gate tool + 1 gsd_skip_slice)');
|
|
49
49
|
|
|
50
50
|
// ─── Both names exist for each pair ──────────────────────────────────────────
|
|
51
51
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Also provides detectFileOverlap() for surfacing downstream impact on quick tasks.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { createRequire } from "node:module";
|
|
16
16
|
import { gsdRoot, milestonesDir } from "./paths.js";
|
|
@@ -129,6 +129,129 @@ export function executeReplan(
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// ─── Backtrack (Milestone Regression) ────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Execute a backtrack directive — user wants to abandon current milestone
|
|
136
|
+
* and return to a previous one (milestone regression).
|
|
137
|
+
*
|
|
138
|
+
* Writes a BACKTRACK-TRIGGER.md marker at `.gsd/BACKTRACK-TRIGGER.md` with
|
|
139
|
+
* the target milestone, reason, and timestamp. The state machine (deriveState)
|
|
140
|
+
* detects this and transitions the project to the target milestone, resetting
|
|
141
|
+
* its slices to allow re-planning.
|
|
142
|
+
*
|
|
143
|
+
* Returns the extracted target milestone ID, or null if extraction failed.
|
|
144
|
+
*/
|
|
145
|
+
export function executeBacktrack(
|
|
146
|
+
basePath: string,
|
|
147
|
+
currentMilestoneId: string,
|
|
148
|
+
capture: CaptureEntry,
|
|
149
|
+
): string | null {
|
|
150
|
+
try {
|
|
151
|
+
// Extract target milestone from capture text or resolution
|
|
152
|
+
const targetMatch = (capture.resolution ?? capture.text)
|
|
153
|
+
.match(/\b(M\d{3}(?:-[a-z0-9]{6})?)\b/);
|
|
154
|
+
const targetMilestoneId = targetMatch?.[1] ?? null;
|
|
155
|
+
|
|
156
|
+
const ts = new Date().toISOString();
|
|
157
|
+
const triggerPath = join(gsdRoot(basePath), "BACKTRACK-TRIGGER.md");
|
|
158
|
+
const content = [
|
|
159
|
+
`# Backtrack Trigger`,
|
|
160
|
+
``,
|
|
161
|
+
`**Source:** Capture ${capture.id}`,
|
|
162
|
+
`**Capture:** ${capture.text}`,
|
|
163
|
+
`**Rationale:** ${capture.rationale ?? "User-initiated milestone backtrack"}`,
|
|
164
|
+
`**From:** ${currentMilestoneId}`,
|
|
165
|
+
`**Target:** ${targetMilestoneId ?? "(user to specify)"}`,
|
|
166
|
+
`**Triggered:** ${ts}`,
|
|
167
|
+
``,
|
|
168
|
+
`Auto-mode was paused by this backtrack directive. The user directed`,
|
|
169
|
+
`that the current milestone (${currentMilestoneId}) be abandoned and work`,
|
|
170
|
+
`should return to ${targetMilestoneId ?? "a previous milestone"}.`,
|
|
171
|
+
``,
|
|
172
|
+
`## Recovery Steps`,
|
|
173
|
+
``,
|
|
174
|
+
`1. Review what went wrong in ${currentMilestoneId}`,
|
|
175
|
+
`2. Identify missing features/requirements from the target milestone`,
|
|
176
|
+
`3. Resume auto-mode — the state machine will re-enter discussion for the target`,
|
|
177
|
+
].join("\n");
|
|
178
|
+
|
|
179
|
+
writeFileSync(triggerPath, content, "utf-8");
|
|
180
|
+
|
|
181
|
+
// If we have a valid target, also reset that milestone's completion status
|
|
182
|
+
// so deriveState() will re-enter it as the active milestone.
|
|
183
|
+
if (targetMilestoneId) {
|
|
184
|
+
try {
|
|
185
|
+
const targetDir = join(milestonesDir(basePath), targetMilestoneId);
|
|
186
|
+
if (existsSync(targetDir)) {
|
|
187
|
+
// Write a regression marker so the state machine knows this milestone
|
|
188
|
+
// needs re-discussion, not just re-execution
|
|
189
|
+
const regressionPath = join(targetDir, `${targetMilestoneId}-REGRESSION.md`);
|
|
190
|
+
writeFileSync(regressionPath, [
|
|
191
|
+
`# Milestone Regression`,
|
|
192
|
+
``,
|
|
193
|
+
`**From:** ${currentMilestoneId}`,
|
|
194
|
+
`**Reason:** ${capture.text}`,
|
|
195
|
+
`**Triggered:** ${ts}`,
|
|
196
|
+
``,
|
|
197
|
+
`This milestone is being revisited because downstream milestone`,
|
|
198
|
+
`${currentMilestoneId} failed or missed critical features that should`,
|
|
199
|
+
`have been part of this milestone's scope.`,
|
|
200
|
+
``,
|
|
201
|
+
`The discuss phase should re-evaluate requirements and identify gaps.`,
|
|
202
|
+
].join("\n"), "utf-8");
|
|
203
|
+
}
|
|
204
|
+
} catch { /* best-effort */ }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return targetMilestoneId;
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Read the backtrack trigger file if it exists.
|
|
215
|
+
* Returns the parsed target milestone and metadata, or null.
|
|
216
|
+
*/
|
|
217
|
+
export function readBacktrackTrigger(basePath: string): {
|
|
218
|
+
target: string | null;
|
|
219
|
+
from: string | null;
|
|
220
|
+
capture: string;
|
|
221
|
+
triggeredAt: string;
|
|
222
|
+
} | null {
|
|
223
|
+
const triggerPath = join(gsdRoot(basePath), "BACKTRACK-TRIGGER.md");
|
|
224
|
+
if (!existsSync(triggerPath)) return null;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const content = readFileSync(triggerPath, "utf-8");
|
|
228
|
+
const target = content.match(/\*\*Target:\*\*\s*(.+)/)?.[1]?.trim() ?? null;
|
|
229
|
+
const from = content.match(/\*\*From:\*\*\s*(.+)/)?.[1]?.trim() ?? null;
|
|
230
|
+
const capture = content.match(/\*\*Capture:\*\*\s*(.+)/)?.[1]?.trim() ?? "";
|
|
231
|
+
const triggeredAt = content.match(/\*\*Triggered:\*\*\s*(.+)/)?.[1]?.trim() ?? "";
|
|
232
|
+
return {
|
|
233
|
+
target: target === "(user to specify)" ? null : target,
|
|
234
|
+
from,
|
|
235
|
+
capture,
|
|
236
|
+
triggeredAt,
|
|
237
|
+
};
|
|
238
|
+
} catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Remove the backtrack trigger after it has been processed.
|
|
245
|
+
*/
|
|
246
|
+
export function clearBacktrackTrigger(basePath: string): void {
|
|
247
|
+
const triggerPath = join(gsdRoot(basePath), "BACKTRACK-TRIGGER.md");
|
|
248
|
+
try {
|
|
249
|
+
if (existsSync(triggerPath)) {
|
|
250
|
+
unlinkSync(triggerPath);
|
|
251
|
+
}
|
|
252
|
+
} catch { /* best-effort */ }
|
|
253
|
+
}
|
|
254
|
+
|
|
132
255
|
// ─── File Overlap Detection ───────────────────────────────────────────────────
|
|
133
256
|
|
|
134
257
|
/**
|
|
@@ -298,6 +421,10 @@ export interface TriageExecutionResult {
|
|
|
298
421
|
deferredMilestones: number;
|
|
299
422
|
/** Captures classified as quick-task that need dispatch */
|
|
300
423
|
quickTasks: CaptureEntry[];
|
|
424
|
+
/** Number of stop directives (will pause auto-mode via guard) */
|
|
425
|
+
stopped: number;
|
|
426
|
+
/** Backtrack captures (will trigger milestone regression via guard) */
|
|
427
|
+
backtracks: CaptureEntry[];
|
|
301
428
|
/** Details of each action taken, for logging */
|
|
302
429
|
actions: string[];
|
|
303
430
|
}
|
|
@@ -326,6 +453,8 @@ export function executeTriageResolutions(
|
|
|
326
453
|
replanned: 0,
|
|
327
454
|
deferredMilestones: 0,
|
|
328
455
|
quickTasks: [],
|
|
456
|
+
stopped: 0,
|
|
457
|
+
backtracks: [],
|
|
329
458
|
actions: [],
|
|
330
459
|
};
|
|
331
460
|
|
|
@@ -409,5 +538,19 @@ export function executeTriageResolutions(
|
|
|
409
538
|
}
|
|
410
539
|
}
|
|
411
540
|
|
|
541
|
+
// Count stop/backtrack captures — these are handled by the pre-dispatch guard
|
|
542
|
+
// in runGuards(), not here. We just report them for logging purposes.
|
|
543
|
+
const allCaptures = loadAllCaptures(basePath);
|
|
544
|
+
for (const cap of allCaptures) {
|
|
545
|
+
if (cap.status !== "resolved" || cap.executed) continue;
|
|
546
|
+
if (cap.classification === "stop") {
|
|
547
|
+
result.stopped++;
|
|
548
|
+
result.actions.push(`Stop directive from ${cap.id}: "${cap.text}" — will pause on next dispatch`);
|
|
549
|
+
} else if (cap.classification === "backtrack") {
|
|
550
|
+
result.backtracks.push(cap);
|
|
551
|
+
result.actions.push(`Backtrack directive from ${cap.id}: "${cap.text}" — will trigger milestone regression on next dispatch`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
412
555
|
return result;
|
|
413
556
|
}
|
|
@@ -49,10 +49,18 @@ const CLASSIFICATION_LABELS: Record<Classification, { label: string; description
|
|
|
49
49
|
label: "Note",
|
|
50
50
|
description: "Informational only — no action needed.",
|
|
51
51
|
},
|
|
52
|
+
"stop": {
|
|
53
|
+
label: "Stop",
|
|
54
|
+
description: "Halt auto-mode immediately — user directive to cease execution.",
|
|
55
|
+
},
|
|
56
|
+
"backtrack": {
|
|
57
|
+
label: "Backtrack",
|
|
58
|
+
description: "Abandon current milestone and return to a previous one.",
|
|
59
|
+
},
|
|
52
60
|
};
|
|
53
61
|
|
|
54
62
|
const ALL_CLASSIFICATIONS: Classification[] = [
|
|
55
|
-
"quick-task", "inject", "defer", "replan", "note",
|
|
63
|
+
"quick-task", "inject", "defer", "replan", "note", "stop", "backtrack",
|
|
56
64
|
];
|
|
57
65
|
|
|
58
66
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
@@ -83,8 +91,9 @@ export async function showTriageConfirmation(
|
|
|
83
91
|
const capture = captureMap.get(result.captureId);
|
|
84
92
|
if (!capture) continue;
|
|
85
93
|
|
|
86
|
-
// Auto-confirm note and
|
|
87
|
-
if (result.classification === "note" || result.classification === "defer"
|
|
94
|
+
// Auto-confirm note, defer, stop, and backtrack — low-impact or urgent directives
|
|
95
|
+
if (result.classification === "note" || result.classification === "defer"
|
|
96
|
+
|| result.classification === "stop" || result.classification === "backtrack") {
|
|
88
97
|
const resolution = result.classification === "note"
|
|
89
98
|
? "acknowledged as note"
|
|
90
99
|
: `deferred${result.targetSlice ? ` to ${result.targetSlice}` : ""}`;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: btw
|
|
3
|
+
description: Ask a quick side question about your current work without derailing the main task. Answers from existing conversation context only — no tool calls, no file reads, single concise response. Use when you need a fast answer from what is already in this session.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<objective>
|
|
7
|
+
Answer a quick side question using only what is already present in the current conversation context. Do not read files, run commands, search, or use any tools. Give a single, concise response and return focus to the main work.
|
|
8
|
+
</objective>
|
|
9
|
+
|
|
10
|
+
<behavior>
|
|
11
|
+
**This is a side question, not a task.**
|
|
12
|
+
|
|
13
|
+
- Answer only from information already in the conversation (files read, decisions made, code seen, context established)
|
|
14
|
+
- Do NOT use any tools — no Read, no Bash, no Grep, no Search
|
|
15
|
+
- If the answer requires reading something new, say so briefly and suggest the user ask as a normal prompt instead
|
|
16
|
+
- Keep the response short and direct — one to a few sentences unless the question genuinely needs more
|
|
17
|
+
- Do not summarize the main work, ask follow-up questions, or offer to do anything else
|
|
18
|
+
- After answering, stop — do not prompt for next steps
|
|
19
|
+
</behavior>
|
|
20
|
+
|
|
21
|
+
<quick_start>
|
|
22
|
+
Parse the argument after `/btw` as the question. Answer it directly from context.
|
|
23
|
+
|
|
24
|
+
If no argument is provided, ask: "What did you want to know?"
|
|
25
|
+
|
|
26
|
+
If the question cannot be answered from current context (requires reading a file, running a command, or information not yet in the session), respond with:
|
|
27
|
+
"I'd need to [read X / run Y / look up Z] to answer that — ask it as a normal prompt when you're ready."
|
|
28
|
+
</quick_start>
|
|
29
|
+
|
|
30
|
+
<examples>
|
|
31
|
+
**Good uses of /btw:**
|
|
32
|
+
- `/btw what was the name of that config file again?` → answers from files already read in session
|
|
33
|
+
- `/btw which branch are we on?` → answers from git context already established
|
|
34
|
+
- `/btw did we already handle the null case in that function?` → answers from code already reviewed
|
|
35
|
+
- `/btw what model does this use?` → answers from code or config already in context
|
|
36
|
+
|
|
37
|
+
**Not a good fit for /btw (suggest normal prompt):**
|
|
38
|
+
- Questions requiring reading a file not yet seen
|
|
39
|
+
- Questions requiring running a command
|
|
40
|
+
- Questions needing a multi-step answer or follow-up
|
|
41
|
+
- Starting a new task or changing direction
|
|
42
|
+
</examples>
|
|
File without changes
|
|
File without changes
|