gsd-pi 2.59.0-dev.023bd39 → 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/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- 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 +14 -14
- 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/dist/web/standalone/.next/static/{QlWL-8CXgQpzV3ehkNMzh → t_cBZAENjaOJIRST3dw08}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{QlWL-8CXgQpzV3ehkNMzh → t_cBZAENjaOJIRST3dw08}/_ssgManifest.js +0 -0
|
@@ -15,7 +15,7 @@ import { gsdRoot } from "./paths.js";
|
|
|
15
15
|
|
|
16
16
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
17
17
|
|
|
18
|
-
export type Classification = "quick-task" | "inject" | "defer" | "replan" | "note";
|
|
18
|
+
export type Classification = "quick-task" | "inject" | "defer" | "replan" | "note" | "stop" | "backtrack";
|
|
19
19
|
|
|
20
20
|
export interface CaptureEntry {
|
|
21
21
|
id: string;
|
|
@@ -42,7 +42,7 @@ export interface TriageResult {
|
|
|
42
42
|
|
|
43
43
|
const CAPTURES_FILENAME = "CAPTURES.md";
|
|
44
44
|
const VALID_CLASSIFICATIONS: readonly string[] = [
|
|
45
|
-
"quick-task", "inject", "defer", "replan", "note",
|
|
45
|
+
"quick-task", "inject", "defer", "replan", "note", "stop", "backtrack",
|
|
46
46
|
];
|
|
47
47
|
|
|
48
48
|
// ─── Path Resolution ──────────────────────────────────────────────────────────
|
|
@@ -285,6 +285,75 @@ export function loadActionableCaptures(basePath: string, currentMilestoneId?: st
|
|
|
285
285
|
);
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Load unexecuted stop captures — user directives to halt auto-mode.
|
|
290
|
+
* These are checked in the pre-dispatch guard pipeline (runGuards) to
|
|
291
|
+
* pause auto-mode before the next unit is dispatched.
|
|
292
|
+
*/
|
|
293
|
+
export function loadStopCaptures(basePath: string): CaptureEntry[] {
|
|
294
|
+
return loadAllCaptures(basePath).filter(
|
|
295
|
+
c => c.status === "resolved" && !c.executed &&
|
|
296
|
+
(c.classification === "stop" || c.classification === "backtrack"),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Load unexecuted backtrack captures specifically — captures directing
|
|
302
|
+
* auto-mode to abandon current milestone and return to a previous one.
|
|
303
|
+
*/
|
|
304
|
+
export function loadBacktrackCaptures(basePath: string): CaptureEntry[] {
|
|
305
|
+
return loadAllCaptures(basePath).filter(
|
|
306
|
+
c => c.status === "resolved" && !c.executed && c.classification === "backtrack",
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Revert captures that were silenced by non-triage agents.
|
|
312
|
+
*
|
|
313
|
+
* When an execute-task or other non-triage agent writes `**Status:** resolved`
|
|
314
|
+
* to CAPTURES.md, it bypasses the triage pipeline entirely. This function
|
|
315
|
+
* detects such captures (resolved but missing the Classification field that
|
|
316
|
+
* triage always writes) and reverts them to pending so the triage sidecar
|
|
317
|
+
* picks them up properly.
|
|
318
|
+
*
|
|
319
|
+
* Returns the number of captures reverted.
|
|
320
|
+
*/
|
|
321
|
+
export function revertExecutorResolvedCaptures(basePath: string): number {
|
|
322
|
+
const filePath = resolveCapturesPath(basePath);
|
|
323
|
+
if (!existsSync(filePath)) return 0;
|
|
324
|
+
|
|
325
|
+
let content = readFileSync(filePath, "utf-8");
|
|
326
|
+
let reverted = 0;
|
|
327
|
+
|
|
328
|
+
const all = loadAllCaptures(basePath);
|
|
329
|
+
for (const capture of all) {
|
|
330
|
+
// A properly triaged capture has both resolved status AND a classification.
|
|
331
|
+
// An executor-silenced capture has resolved status but NO classification.
|
|
332
|
+
if (capture.status === "resolved" && !capture.classification) {
|
|
333
|
+
const sectionRegex = new RegExp(
|
|
334
|
+
`(### ${escapeRegex(capture.id)}\\n(?:(?!### ).)*?)(?=### |$)`,
|
|
335
|
+
"s",
|
|
336
|
+
);
|
|
337
|
+
const match = sectionRegex.exec(content);
|
|
338
|
+
if (match) {
|
|
339
|
+
let section = match[1];
|
|
340
|
+
section = section.replace(
|
|
341
|
+
/\*\*Status:\*\*\s*resolved/i,
|
|
342
|
+
"**Status:** pending",
|
|
343
|
+
);
|
|
344
|
+
content = content.replace(sectionRegex, section);
|
|
345
|
+
reverted++;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (reverted > 0) {
|
|
351
|
+
writeFileSync(filePath, content, "utf-8");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return reverted;
|
|
355
|
+
}
|
|
356
|
+
|
|
288
357
|
/**
|
|
289
358
|
* Retroactively stamp a capture with a milestone ID.
|
|
290
359
|
*
|
|
@@ -212,7 +212,7 @@ function analyzePlanComplexity(
|
|
|
212
212
|
/**
|
|
213
213
|
* Extract task metadata from the task plan file on disk.
|
|
214
214
|
*/
|
|
215
|
-
function extractTaskMetadata(unitId: string, basePath: string): TaskMetadata {
|
|
215
|
+
export function extractTaskMetadata(unitId: string, basePath: string): TaskMetadata {
|
|
216
216
|
const meta: TaskMetadata = {};
|
|
217
217
|
const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
|
|
218
218
|
if (!mid || !sid || !tid) return meta;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observation masking for GSD auto-mode sessions.
|
|
3
|
+
*
|
|
4
|
+
* Replaces tool result content older than N turns with a placeholder.
|
|
5
|
+
* Reduces context bloat between compactions with zero LLM overhead.
|
|
6
|
+
* Preserves message ordering, roles, and all assistant/user messages.
|
|
7
|
+
*
|
|
8
|
+
* Operates on the pi-ai Message[] format (post-convertToLlm, pre-provider):
|
|
9
|
+
* - toolResult messages: { role: "toolResult", content: TextContent[] }
|
|
10
|
+
* - bash results are already converted to: { role: "user", content: [{type:"text",text:"..."}] }
|
|
11
|
+
* and start with "Ran `" from bashExecutionToText.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
interface MaskableMessage {
|
|
15
|
+
role: string;
|
|
16
|
+
content: unknown;
|
|
17
|
+
type?: string;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const MASK_PLACEHOLDER = "[result masked — within summarized history]";
|
|
22
|
+
const MASK_CONTENT_BLOCK = [{ type: "text" as const, text: MASK_PLACEHOLDER }];
|
|
23
|
+
|
|
24
|
+
function findTurnBoundary(messages: MaskableMessage[], keepRecentTurns: number): number {
|
|
25
|
+
let turnsSeen = 0;
|
|
26
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
27
|
+
const m = messages[i];
|
|
28
|
+
// In the LLM payload, genuine user turns have role "user".
|
|
29
|
+
// Tool results have role "toolResult" and are excluded by this check.
|
|
30
|
+
if (m.role === "user") {
|
|
31
|
+
// Skip bash-result user messages (converted from bashExecution) — these aren't real user turns
|
|
32
|
+
if (isBashResultUserMessage(m)) continue;
|
|
33
|
+
turnsSeen++;
|
|
34
|
+
if (turnsSeen >= keepRecentTurns) return i;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Detect user messages that originated from bashExecution.
|
|
42
|
+
* After convertToLlm, these are {role: "user", content: [{type:"text", text:"Ran `cmd`\n..."}]}.
|
|
43
|
+
* The bashExecutionToText format always starts with "Ran `".
|
|
44
|
+
*/
|
|
45
|
+
function isBashResultUserMessage(m: MaskableMessage): boolean {
|
|
46
|
+
if (m.role !== "user" || !Array.isArray(m.content)) return false;
|
|
47
|
+
const first = m.content[0];
|
|
48
|
+
return first && typeof first === "object" && "text" in first &&
|
|
49
|
+
typeof first.text === "string" && first.text.startsWith("Ran `");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isMaskableMessage(m: MaskableMessage): boolean {
|
|
53
|
+
// Tool result messages (role: "toolResult" in pi-ai format)
|
|
54
|
+
if (m.role === "toolResult") return true;
|
|
55
|
+
// Bash-result user messages (converted from bashExecution by convertToLlm)
|
|
56
|
+
if (isBashResultUserMessage(m)) return true;
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createObservationMask(keepRecentTurns: number = 8) {
|
|
61
|
+
return (messages: MaskableMessage[]): MaskableMessage[] => {
|
|
62
|
+
const boundary = findTurnBoundary(messages, keepRecentTurns);
|
|
63
|
+
if (boundary === 0) return messages;
|
|
64
|
+
|
|
65
|
+
return messages.map((m, i) => {
|
|
66
|
+
if (i >= boundary) return m;
|
|
67
|
+
if (isMaskableMessage(m)) {
|
|
68
|
+
// Content may be string or array of content blocks — always replace with array
|
|
69
|
+
return { ...m, content: MASK_CONTENT_BLOCK };
|
|
70
|
+
}
|
|
71
|
+
return m;
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -189,6 +189,13 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
|
|
|
189
189
|
- `budget_pressure`: boolean — downgrade model tier when budget is under pressure. Default: `true`.
|
|
190
190
|
- `cross_provider`: boolean — allow routing across different providers. Default: `true`.
|
|
191
191
|
- `hooks`: boolean — enable routing hooks. Default: `true`.
|
|
192
|
+
- `capability_routing`: boolean — enable capability-profile scoring for model selection within a tier. Requires `enabled: true`. Default: `false`.
|
|
193
|
+
|
|
194
|
+
- `context_management`: configures context hygiene for auto-mode sessions. Keys:
|
|
195
|
+
- `observation_masking`: boolean — mask old tool results to reduce context bloat. Default: `true`.
|
|
196
|
+
- `observation_mask_turns`: number — keep this many recent turns verbatim (1-50). Default: `8`.
|
|
197
|
+
- `compaction_threshold_percent`: number — trigger compaction at this % of context window (0.5-0.95). Lower values fire compaction earlier, reducing drift. Default: `0.70`.
|
|
198
|
+
- `tool_result_max_chars`: number — max chars per tool result in GSD sessions (200-10000). Default: `800`.
|
|
192
199
|
|
|
193
200
|
- `auto_visualize`: boolean — show a visualizer hint after each milestone completion in auto-mode. Default: `false`.
|
|
194
201
|
|
|
@@ -1661,11 +1661,11 @@ export function getActiveSliceFromDb(milestoneId: string): SliceRow | null {
|
|
|
1661
1661
|
const row = currentDb.prepare(
|
|
1662
1662
|
`SELECT s.* FROM slices s
|
|
1663
1663
|
WHERE s.milestone_id = :mid
|
|
1664
|
-
AND s.status NOT IN ('complete', 'done')
|
|
1664
|
+
AND s.status NOT IN ('complete', 'done', 'skipped')
|
|
1665
1665
|
AND NOT EXISTS (
|
|
1666
1666
|
SELECT 1 FROM json_each(s.depends) AS dep
|
|
1667
1667
|
WHERE dep.value NOT IN (
|
|
1668
|
-
SELECT id FROM slices WHERE milestone_id = :mid AND status IN ('complete', 'done')
|
|
1668
|
+
SELECT id FROM slices WHERE milestone_id = :mid AND status IN ('complete', 'done', 'skipped')
|
|
1669
1669
|
)
|
|
1670
1670
|
)
|
|
1671
1671
|
ORDER BY s.sequence, s.id
|
|
@@ -10,6 +10,7 @@ import type { ResolvedModelConfig } from "./preferences.js";
|
|
|
10
10
|
|
|
11
11
|
export interface DynamicRoutingConfig {
|
|
12
12
|
enabled?: boolean;
|
|
13
|
+
capability_routing?: boolean; // default: false — enable capability profile scoring
|
|
13
14
|
tier_models?: {
|
|
14
15
|
light?: string;
|
|
15
16
|
standard?: string;
|
|
@@ -32,6 +33,12 @@ export interface RoutingDecision {
|
|
|
32
33
|
wasDowngraded: boolean;
|
|
33
34
|
/** Human-readable reason for this decision */
|
|
34
35
|
reason: string;
|
|
36
|
+
/** How the model was selected. */
|
|
37
|
+
selectionMethod?: "tier-only" | "capability-scored";
|
|
38
|
+
/** Capability scores per model (when capability-scored). */
|
|
39
|
+
capabilityScores?: Record<string, number>;
|
|
40
|
+
/** Task requirement vector (when capability-scored). */
|
|
41
|
+
taskRequirements?: Partial<Record<string, number>>;
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
// ─── Known Model Tiers ───────────────────────────────────────────────────────
|
|
@@ -114,6 +121,91 @@ const MODEL_COST_PER_1K_INPUT: Record<string, number> = {
|
|
|
114
121
|
"deepseek-chat": 0.00014,
|
|
115
122
|
};
|
|
116
123
|
|
|
124
|
+
// ─── Capability Profiles (ADR-004 Phase 2) ──────────────────────────────────
|
|
125
|
+
// 7-dimension profiles, 0–100 normalized. Models without a profile
|
|
126
|
+
// score 50 uniformly — capability scoring is a no-op for them.
|
|
127
|
+
|
|
128
|
+
export interface ModelCapabilities {
|
|
129
|
+
coding: number;
|
|
130
|
+
debugging: number;
|
|
131
|
+
research: number;
|
|
132
|
+
reasoning: number;
|
|
133
|
+
speed: number;
|
|
134
|
+
longContext: number;
|
|
135
|
+
instruction: number;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const MODEL_CAPABILITY_PROFILES: Record<string, ModelCapabilities> = {
|
|
139
|
+
"claude-opus-4-6": { coding: 95, debugging: 90, research: 85, reasoning: 95, speed: 30, longContext: 80, instruction: 90 },
|
|
140
|
+
"claude-sonnet-4-6": { coding: 85, debugging: 80, research: 75, reasoning: 80, speed: 60, longContext: 75, instruction: 85 },
|
|
141
|
+
"claude-haiku-4-5": { coding: 60, debugging: 50, research: 45, reasoning: 50, speed: 95, longContext: 50, instruction: 75 },
|
|
142
|
+
"gpt-4o": { coding: 80, debugging: 75, research: 70, reasoning: 75, speed: 65, longContext: 70, instruction: 80 },
|
|
143
|
+
"gpt-4o-mini": { coding: 55, debugging: 45, research: 40, reasoning: 45, speed: 90, longContext: 45, instruction: 70 },
|
|
144
|
+
"gemini-2.5-pro": { coding: 75, debugging: 70, research: 85, reasoning: 75, speed: 55, longContext: 90, instruction: 75 },
|
|
145
|
+
"gemini-2.0-flash": { coding: 50, debugging: 40, research: 50, reasoning: 40, speed: 95, longContext: 60, instruction: 65 },
|
|
146
|
+
"deepseek-chat": { coding: 75, debugging: 65, research: 55, reasoning: 70, speed: 70, longContext: 55, instruction: 65 },
|
|
147
|
+
"o3": { coding: 80, debugging: 85, research: 80, reasoning: 92, speed: 25, longContext: 70, instruction: 85 },
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const BASE_REQUIREMENTS: Record<string, Partial<Record<keyof ModelCapabilities, number>>> = {
|
|
151
|
+
"execute-task": { coding: 0.9, instruction: 0.7, speed: 0.3 },
|
|
152
|
+
"research-milestone": { research: 0.9, longContext: 0.7, reasoning: 0.5 },
|
|
153
|
+
"research-slice": { research: 0.9, longContext: 0.7, reasoning: 0.5 },
|
|
154
|
+
"plan-milestone": { reasoning: 0.9, coding: 0.5 },
|
|
155
|
+
"plan-slice": { reasoning: 0.9, coding: 0.5 },
|
|
156
|
+
"replan-slice": { reasoning: 0.9, debugging: 0.6, coding: 0.5 },
|
|
157
|
+
"reassess-roadmap": { reasoning: 0.9, research: 0.5 },
|
|
158
|
+
"complete-slice": { instruction: 0.8, speed: 0.7 },
|
|
159
|
+
"run-uat": { instruction: 0.7, speed: 0.8 },
|
|
160
|
+
"discuss-milestone": { reasoning: 0.6, instruction: 0.7 },
|
|
161
|
+
"complete-milestone": { instruction: 0.8, reasoning: 0.5 },
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Compute a task requirement vector from unit type and optional metadata.
|
|
166
|
+
*/
|
|
167
|
+
export function computeTaskRequirements(
|
|
168
|
+
unitType: string,
|
|
169
|
+
metadata?: { tags?: string[]; complexityKeywords?: string[]; fileCount?: number; estimatedLines?: number },
|
|
170
|
+
): Partial<Record<keyof ModelCapabilities, number>> {
|
|
171
|
+
const base = { ...(BASE_REQUIREMENTS[unitType] ?? { reasoning: 0.5 }) };
|
|
172
|
+
|
|
173
|
+
if (unitType === "execute-task" && metadata) {
|
|
174
|
+
if (metadata.tags?.some(t => /^(docs?|readme|comment|config|typo|rename)$/i.test(t))) {
|
|
175
|
+
return { ...base, instruction: 0.9, coding: 0.3, speed: 0.7 };
|
|
176
|
+
}
|
|
177
|
+
if (metadata.complexityKeywords?.some(k => k === "concurrency" || k === "compatibility")) {
|
|
178
|
+
return { ...base, debugging: 0.9, reasoning: 0.8 };
|
|
179
|
+
}
|
|
180
|
+
if (metadata.complexityKeywords?.some(k => k === "migration" || k === "architecture")) {
|
|
181
|
+
return { ...base, reasoning: 0.9, coding: 0.8 };
|
|
182
|
+
}
|
|
183
|
+
if ((metadata.fileCount ?? 0) >= 6 || (metadata.estimatedLines ?? 0) >= 500) {
|
|
184
|
+
return { ...base, coding: 0.9, reasoning: 0.7 };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return base;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Score a model against a task requirement vector.
|
|
193
|
+
* Returns weighted average in range 0–100. Returns 50 for empty requirements.
|
|
194
|
+
*/
|
|
195
|
+
export function scoreModel(
|
|
196
|
+
capabilities: ModelCapabilities,
|
|
197
|
+
requirements: Partial<Record<keyof ModelCapabilities, number>>,
|
|
198
|
+
): number {
|
|
199
|
+
let weightedSum = 0;
|
|
200
|
+
let weightSum = 0;
|
|
201
|
+
for (const [dim, weight] of Object.entries(requirements)) {
|
|
202
|
+
const capability = capabilities[dim as keyof ModelCapabilities] ?? 50;
|
|
203
|
+
weightedSum += weight * capability;
|
|
204
|
+
weightSum += weight;
|
|
205
|
+
}
|
|
206
|
+
return weightSum > 0 ? weightedSum / weightSum : 50;
|
|
207
|
+
}
|
|
208
|
+
|
|
117
209
|
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
118
210
|
|
|
119
211
|
/**
|
|
@@ -132,6 +224,8 @@ export function resolveModelForComplexity(
|
|
|
132
224
|
phaseConfig: ResolvedModelConfig | undefined,
|
|
133
225
|
routingConfig: DynamicRoutingConfig,
|
|
134
226
|
availableModelIds: string[],
|
|
227
|
+
unitType?: string,
|
|
228
|
+
metadata?: { tags?: string[]; complexityKeywords?: string[]; fileCount?: number; estimatedLines?: number },
|
|
135
229
|
): RoutingDecision {
|
|
136
230
|
// If no phase config or routing disabled, pass through
|
|
137
231
|
if (!phaseConfig || !routingConfig.enabled) {
|
|
@@ -175,25 +269,40 @@ export function resolveModelForComplexity(
|
|
|
175
269
|
}
|
|
176
270
|
|
|
177
271
|
// Find the best model for the requested tier
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
272
|
+
const useCapabilityScoring = routingConfig.capability_routing && unitType;
|
|
273
|
+
|
|
274
|
+
let targetModelId: string | null;
|
|
275
|
+
let capabilityScores: Record<string, number> | undefined;
|
|
276
|
+
let taskRequirements: Partial<Record<string, number>> | undefined;
|
|
277
|
+
let selectionMethod: "tier-only" | "capability-scored" = "tier-only";
|
|
278
|
+
|
|
279
|
+
if (useCapabilityScoring) {
|
|
280
|
+
const result = findModelForTierWithCapability(
|
|
281
|
+
requestedTier, routingConfig, availableModelIds,
|
|
282
|
+
routingConfig.cross_provider !== false, unitType, metadata,
|
|
283
|
+
);
|
|
284
|
+
targetModelId = result.modelId;
|
|
285
|
+
capabilityScores = Object.keys(result.scores).length > 0 ? result.scores : undefined;
|
|
286
|
+
taskRequirements = Object.keys(result.requirements).length > 0 ? result.requirements : undefined;
|
|
287
|
+
selectionMethod = capabilityScores ? "capability-scored" : "tier-only";
|
|
288
|
+
} else {
|
|
289
|
+
targetModelId = findModelForTier(
|
|
290
|
+
requestedTier, routingConfig, availableModelIds,
|
|
291
|
+
routingConfig.cross_provider !== false,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
184
294
|
|
|
185
295
|
if (!targetModelId) {
|
|
186
|
-
// No suitable model found — use configured primary
|
|
187
296
|
return {
|
|
188
297
|
modelId: configuredPrimary,
|
|
189
298
|
fallbacks: phaseConfig.fallbacks,
|
|
190
299
|
tier: requestedTier,
|
|
191
300
|
wasDowngraded: false,
|
|
192
301
|
reason: `no ${requestedTier}-tier model available`,
|
|
302
|
+
selectionMethod,
|
|
193
303
|
};
|
|
194
304
|
}
|
|
195
305
|
|
|
196
|
-
// Build fallback chain: [downgraded_model, ...configured_fallbacks, configured_primary]
|
|
197
306
|
const fallbacks = [
|
|
198
307
|
...phaseConfig.fallbacks.filter(f => f !== targetModelId),
|
|
199
308
|
configuredPrimary,
|
|
@@ -205,6 +314,9 @@ export function resolveModelForComplexity(
|
|
|
205
314
|
tier: requestedTier,
|
|
206
315
|
wasDowngraded: true,
|
|
207
316
|
reason: classification.reason,
|
|
317
|
+
selectionMethod,
|
|
318
|
+
capabilityScores,
|
|
319
|
+
taskRequirements,
|
|
208
320
|
};
|
|
209
321
|
}
|
|
210
322
|
|
|
@@ -226,6 +338,7 @@ export function escalateTier(currentTier: ComplexityTier): ComplexityTier | null
|
|
|
226
338
|
export function defaultRoutingConfig(): DynamicRoutingConfig {
|
|
227
339
|
return {
|
|
228
340
|
enabled: true,
|
|
341
|
+
capability_routing: false,
|
|
229
342
|
escalate_on_failure: true,
|
|
230
343
|
budget_pressure: true,
|
|
231
344
|
cross_provider: true,
|
|
@@ -298,6 +411,56 @@ function findModelForTier(
|
|
|
298
411
|
return candidates[0] ?? null;
|
|
299
412
|
}
|
|
300
413
|
|
|
414
|
+
function findModelForTierWithCapability(
|
|
415
|
+
tier: ComplexityTier,
|
|
416
|
+
config: DynamicRoutingConfig,
|
|
417
|
+
availableModelIds: string[],
|
|
418
|
+
crossProvider: boolean,
|
|
419
|
+
unitType: string,
|
|
420
|
+
metadata?: { tags?: string[]; complexityKeywords?: string[]; fileCount?: number; estimatedLines?: number },
|
|
421
|
+
): { modelId: string | null; scores: Record<string, number>; requirements: Partial<Record<string, number>> } {
|
|
422
|
+
const explicitModel = config.tier_models?.[tier];
|
|
423
|
+
if (explicitModel) {
|
|
424
|
+
const match = availableModelIds.find(id => {
|
|
425
|
+
const bareAvail = id.includes("/") ? id.split("/").pop()! : id;
|
|
426
|
+
const bareExplicit = explicitModel.includes("/") ? explicitModel.split("/").pop()! : explicitModel;
|
|
427
|
+
return bareAvail === bareExplicit || id === explicitModel;
|
|
428
|
+
});
|
|
429
|
+
if (match) return { modelId: match, scores: {}, requirements: {} };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const requirements = computeTaskRequirements(unitType, metadata);
|
|
433
|
+
const candidates = availableModelIds.filter(id => getModelTier(id) === tier);
|
|
434
|
+
if (candidates.length === 0) return { modelId: null, scores: {}, requirements };
|
|
435
|
+
|
|
436
|
+
const scores: Record<string, number> = {};
|
|
437
|
+
for (const id of candidates) {
|
|
438
|
+
const bareId = id.includes("/") ? id.split("/").pop()! : id;
|
|
439
|
+
const profile = getModelProfile(bareId);
|
|
440
|
+
scores[id] = scoreModel(profile, requirements);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
candidates.sort((a, b) => {
|
|
444
|
+
const scoreDiff = scores[b] - scores[a];
|
|
445
|
+
if (Math.abs(scoreDiff) > 2) return scoreDiff;
|
|
446
|
+
if (crossProvider) {
|
|
447
|
+
const costDiff = getModelCost(a) - getModelCost(b);
|
|
448
|
+
if (costDiff !== 0) return costDiff;
|
|
449
|
+
}
|
|
450
|
+
return a.localeCompare(b);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
return { modelId: candidates[0], scores, requirements };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function getModelProfile(bareId: string): ModelCapabilities {
|
|
457
|
+
if (MODEL_CAPABILITY_PROFILES[bareId]) return MODEL_CAPABILITY_PROFILES[bareId];
|
|
458
|
+
for (const [knownId, profile] of Object.entries(MODEL_CAPABILITY_PROFILES)) {
|
|
459
|
+
if (bareId.includes(knownId) || knownId.includes(bareId)) return profile;
|
|
460
|
+
}
|
|
461
|
+
return { coding: 50, debugging: 50, research: 50, reasoning: 50, speed: 50, longContext: 50, instruction: 50 };
|
|
462
|
+
}
|
|
463
|
+
|
|
301
464
|
function getModelCost(modelId: string): number {
|
|
302
465
|
const bareId = modelId.includes("/") ? modelId.split("/").pop()! : modelId;
|
|
303
466
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase handoff anchors — compact structured summaries written between
|
|
3
|
+
* GSD auto-mode phases so downstream agents inherit decisions, blockers,
|
|
4
|
+
* and intent without re-inferring from scratch.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { gsdRoot } from "./paths.js";
|
|
10
|
+
|
|
11
|
+
export interface PhaseAnchor {
|
|
12
|
+
phase: string;
|
|
13
|
+
milestoneId: string;
|
|
14
|
+
generatedAt: string;
|
|
15
|
+
intent: string;
|
|
16
|
+
decisions: string[];
|
|
17
|
+
blockers: string[];
|
|
18
|
+
nextSteps: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function anchorsDir(basePath: string, milestoneId: string): string {
|
|
22
|
+
return join(gsdRoot(basePath), "milestones", milestoneId, "anchors");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function anchorPath(basePath: string, milestoneId: string, phase: string): string {
|
|
26
|
+
return join(anchorsDir(basePath, milestoneId), `${phase}.json`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function writePhaseAnchor(basePath: string, milestoneId: string, anchor: PhaseAnchor): void {
|
|
30
|
+
const dir = anchorsDir(basePath, milestoneId);
|
|
31
|
+
if (!existsSync(dir)) {
|
|
32
|
+
mkdirSync(dir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
writeFileSync(anchorPath(basePath, milestoneId, anchor.phase), JSON.stringify(anchor, null, 2), "utf-8");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function readPhaseAnchor(basePath: string, milestoneId: string, phase: string): PhaseAnchor | null {
|
|
38
|
+
const path = anchorPath(basePath, milestoneId, phase);
|
|
39
|
+
if (!existsSync(path)) return null;
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(readFileSync(path, "utf-8")) as PhaseAnchor;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatAnchorForPrompt(anchor: PhaseAnchor): string {
|
|
48
|
+
const lines: string[] = [
|
|
49
|
+
`## Handoff from ${anchor.phase}`,
|
|
50
|
+
"",
|
|
51
|
+
`**Intent:** ${anchor.intent}`,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
if (anchor.decisions.length > 0) {
|
|
55
|
+
lines.push("", "**Decisions:**");
|
|
56
|
+
for (const d of anchor.decisions) lines.push(`- ${d}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (anchor.blockers.length > 0) {
|
|
60
|
+
lines.push("", "**Blockers:**");
|
|
61
|
+
for (const b of anchor.blockers) lines.push(`- ${b}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (anchor.nextSteps.length > 0) {
|
|
65
|
+
lines.push("", "**Next steps:**");
|
|
66
|
+
for (const s of anchor.nextSteps) lines.push(`- ${s}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
lines.push("", "---");
|
|
70
|
+
return lines.join("\n");
|
|
71
|
+
}
|
|
@@ -21,6 +21,13 @@ import type {
|
|
|
21
21
|
GateEvaluationConfig,
|
|
22
22
|
} from "./types.js";
|
|
23
23
|
import type { DynamicRoutingConfig } from "./model-router.js";
|
|
24
|
+
|
|
25
|
+
export interface ContextManagementConfig {
|
|
26
|
+
observation_masking?: boolean; // default: true
|
|
27
|
+
observation_mask_turns?: number; // default: 8, range: 1-50
|
|
28
|
+
compaction_threshold_percent?: number; // default: 0.70, range: 0.5-0.95
|
|
29
|
+
tool_result_max_chars?: number; // default: 800, range: 200-10000
|
|
30
|
+
}
|
|
24
31
|
import type { GitHubSyncConfig } from "../github-sync/types.js";
|
|
25
32
|
|
|
26
33
|
// ─── Workflow Modes ──────────────────────────────────────────────────────────
|
|
@@ -94,6 +101,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
|
|
|
94
101
|
"forensics_dedup",
|
|
95
102
|
"show_token_cost",
|
|
96
103
|
"stale_commit_threshold_minutes",
|
|
104
|
+
"context_management",
|
|
97
105
|
"experimental",
|
|
98
106
|
]);
|
|
99
107
|
|
|
@@ -227,6 +235,7 @@ export interface GSDPreferences {
|
|
|
227
235
|
post_unit_hooks?: PostUnitHookConfig[];
|
|
228
236
|
pre_dispatch_hooks?: PreDispatchHookConfig[];
|
|
229
237
|
dynamic_routing?: DynamicRoutingConfig;
|
|
238
|
+
context_management?: ContextManagementConfig;
|
|
230
239
|
token_profile?: TokenProfile;
|
|
231
240
|
phases?: PhaseSkipPreferences;
|
|
232
241
|
auto_visualize?: boolean;
|
|
@@ -428,6 +428,10 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
428
428
|
if (typeof dr.hooks === "boolean") validDr.hooks = dr.hooks;
|
|
429
429
|
else errors.push("dynamic_routing.hooks must be a boolean");
|
|
430
430
|
}
|
|
431
|
+
if (dr.capability_routing !== undefined) {
|
|
432
|
+
if (typeof dr.capability_routing === "boolean") validDr.capability_routing = dr.capability_routing;
|
|
433
|
+
else errors.push("dynamic_routing.capability_routing must be a boolean");
|
|
434
|
+
}
|
|
431
435
|
if (dr.tier_models !== undefined) {
|
|
432
436
|
if (typeof dr.tier_models === "object" && dr.tier_models !== null) {
|
|
433
437
|
const tm = dr.tier_models as Record<string, unknown>;
|
|
@@ -452,6 +456,40 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
452
456
|
}
|
|
453
457
|
}
|
|
454
458
|
|
|
459
|
+
// ─── Context Management ──────────────────────────────────────────────
|
|
460
|
+
if (preferences.context_management !== undefined) {
|
|
461
|
+
if (typeof preferences.context_management === "object" && preferences.context_management !== null) {
|
|
462
|
+
const cm = preferences.context_management as unknown as Record<string, unknown>;
|
|
463
|
+
const validCm: Record<string, unknown> = {};
|
|
464
|
+
|
|
465
|
+
if (cm.observation_masking !== undefined) {
|
|
466
|
+
if (typeof cm.observation_masking === "boolean") validCm.observation_masking = cm.observation_masking;
|
|
467
|
+
else errors.push("context_management.observation_masking must be a boolean");
|
|
468
|
+
}
|
|
469
|
+
if (cm.observation_mask_turns !== undefined) {
|
|
470
|
+
const turns = cm.observation_mask_turns;
|
|
471
|
+
if (typeof turns === "number" && turns >= 1 && turns <= 50) validCm.observation_mask_turns = turns;
|
|
472
|
+
else errors.push("context_management.observation_mask_turns must be a number between 1 and 50");
|
|
473
|
+
}
|
|
474
|
+
if (cm.compaction_threshold_percent !== undefined) {
|
|
475
|
+
const pct = cm.compaction_threshold_percent;
|
|
476
|
+
if (typeof pct === "number" && pct >= 0.5 && pct <= 0.95) validCm.compaction_threshold_percent = pct;
|
|
477
|
+
else errors.push("context_management.compaction_threshold_percent must be a number between 0.5 and 0.95");
|
|
478
|
+
}
|
|
479
|
+
if (cm.tool_result_max_chars !== undefined) {
|
|
480
|
+
const chars = cm.tool_result_max_chars;
|
|
481
|
+
if (typeof chars === "number" && chars >= 200 && chars <= 10000) validCm.tool_result_max_chars = chars;
|
|
482
|
+
else errors.push("context_management.tool_result_max_chars must be a number between 200 and 10000");
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (Object.keys(validCm).length > 0) {
|
|
486
|
+
validated.context_management = validCm as any;
|
|
487
|
+
}
|
|
488
|
+
} else {
|
|
489
|
+
errors.push("context_management must be an object");
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
455
493
|
// ─── Parallel Config ────────────────────────────────────────────────────
|
|
456
494
|
if (preferences.parallel && typeof preferences.parallel === "object") {
|
|
457
495
|
const p = preferences.parallel as unknown as Record<string, unknown>;
|
|
@@ -45,6 +45,13 @@ reason: "<reason>"
|
|
|
45
45
|
### Unpark a milestone
|
|
46
46
|
Remove the `{ID}-PARKED.md` file from the milestone directory to reactivate it.
|
|
47
47
|
|
|
48
|
+
### Skip a slice
|
|
49
|
+
Mark a slice as skipped so auto-mode advances past it without executing. Use the `gsd_skip_slice` tool:
|
|
50
|
+
```
|
|
51
|
+
gsd_skip_slice({ milestone_id: "M003", slice_id: "S02", reason: "Descoped — feature moved to M005" })
|
|
52
|
+
```
|
|
53
|
+
Skipped slices are treated as closed by the state machine (like "complete" but distinct). Use when a slice is no longer needed or has been superseded. The slice data is preserved for reference.
|
|
54
|
+
|
|
48
55
|
### Discard a milestone
|
|
49
56
|
**Permanently** delete a milestone directory and prune it from QUEUE-ORDER.json. **Always confirm with the user before discarding.** Warn explicitly if the milestone has completed work.
|
|
50
57
|
|
|
@@ -20,6 +20,8 @@ The user captured thoughts during execution using `/gsd capture`. Your job is to
|
|
|
20
20
|
|
|
21
21
|
For each capture, classify it as one of:
|
|
22
22
|
|
|
23
|
+
- **stop**: User directive to halt auto-mode immediately. Use when the user says "stop", "halt", "abort", "don't continue", "pause", or otherwise wants execution to cease. Auto-mode will pause after the current unit completes. Examples: "stop running", "halt execution", "don't continue".
|
|
24
|
+
- **backtrack**: User directive to abandon the current milestone and return to a previous one. The user believes earlier milestones missed critical features or need rework. Include the target milestone ID (e.g., M003) in the Resolution field. Auto-mode will pause and write a regression marker. Examples: "restart from M003", "go back to milestone 3", "M004 and M005 failed, restart from M003".
|
|
23
25
|
- **quick-task**: Small, self-contained, no downstream impact. Can be done in minutes without modifying the plan. Examples: fix a typo, add a missing import, tweak a config value.
|
|
24
26
|
- **inject**: Belongs in the current slice but wasn't planned. Needs a new task added to the slice plan. Examples: add error handling to a module being built, add a missing test case for current work.
|
|
25
27
|
- **defer**: Belongs in a future slice or milestone. Not urgent for current work. Examples: performance optimization, feature that depends on unbuilt infrastructure, nice-to-have enhancement.
|
|
@@ -28,10 +30,12 @@ For each capture, classify it as one of:
|
|
|
28
30
|
|
|
29
31
|
## Decision Guidelines
|
|
30
32
|
|
|
33
|
+
- **ALWAYS classify as stop** when the user explicitly says "stop", "halt", "abort", or "don't continue". Never shoe-horn a stop directive into "replan" or "note".
|
|
34
|
+
- **ALWAYS classify as backtrack** when the user references returning to a previous milestone, restarting from an earlier point, or abandoning current milestone work. Include the target milestone ID in the Resolution field (e.g., "Backtrack to M003").
|
|
31
35
|
- Prefer **quick-task** when the work is clearly small and self-contained.
|
|
32
36
|
- Prefer **inject** over **replan** when only a new task is needed, not rewriting existing ones.
|
|
33
37
|
- Prefer **defer** over **inject** when the work doesn't belong in the current slice's scope.
|
|
34
|
-
- Use **replan** only when remaining incomplete tasks need to change — not
|
|
38
|
+
- Use **replan** only when remaining incomplete tasks in the *current slice* need to change — not for cross-milestone issues.
|
|
35
39
|
- Use **note** for observations that don't require action.
|
|
36
40
|
- When unsure between quick-task and inject, consider: will this take more than 10 minutes? If yes, inject.
|
|
37
41
|
|
|
@@ -46,6 +50,7 @@ For each capture, classify it as one of:
|
|
|
46
50
|
- If applicable, which files would be affected
|
|
47
51
|
|
|
48
52
|
For captures classified as **note** or **defer**, auto-confirm without asking — these are low-impact.
|
|
53
|
+
For captures classified as **stop** or **backtrack**, auto-confirm without asking — these are urgent user directives that must be honored immediately.
|
|
49
54
|
For captures classified as **quick-task**, **inject**, or **replan**, ask the user to confirm or choose a different classification.
|
|
50
55
|
|
|
51
56
|
3. **Update** `.gsd/CAPTURES.md` — for each capture, update its section with the confirmed classification:
|