gsd-pi 2.16.0 → 2.18.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/README.md +39 -0
- package/dist/onboarding.js +2 -2
- package/dist/remote-questions-config.d.ts +10 -0
- package/dist/remote-questions-config.js +36 -0
- package/dist/resources/extensions/gsd/activity-log.ts +37 -7
- package/dist/resources/extensions/gsd/auto-dashboard.ts +4 -0
- package/dist/resources/extensions/gsd/auto-dispatch.ts +9 -3
- package/dist/resources/extensions/gsd/auto-prompts.ts +91 -42
- package/dist/resources/extensions/gsd/auto-recovery.ts +7 -2
- package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/dist/resources/extensions/gsd/auto.ts +177 -25
- package/dist/resources/extensions/gsd/commands.ts +264 -23
- package/dist/resources/extensions/gsd/complexity.ts +236 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +202 -2
- package/dist/resources/extensions/gsd/files.ts +129 -3
- package/dist/resources/extensions/gsd/git-service.ts +19 -8
- package/dist/resources/extensions/gsd/gitignore.ts +41 -2
- package/dist/resources/extensions/gsd/guided-flow.ts +247 -10
- package/dist/resources/extensions/gsd/index.ts +47 -3
- package/dist/resources/extensions/gsd/metrics.ts +44 -0
- package/dist/resources/extensions/gsd/native-git-bridge.ts +5 -0
- package/dist/resources/extensions/gsd/native-parser-bridge.ts +5 -0
- package/dist/resources/extensions/gsd/paths.ts +9 -0
- package/dist/resources/extensions/gsd/preferences.ts +181 -2
- package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/dist/resources/extensions/gsd/prompts/system.md +2 -0
- package/dist/resources/extensions/gsd/queue-order.ts +231 -0
- package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/dist/resources/extensions/gsd/routing-history.ts +290 -0
- package/dist/resources/extensions/gsd/state.ts +15 -3
- package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/dist/resources/extensions/gsd/tests/budget-prediction.test.ts +220 -0
- package/dist/resources/extensions/gsd/tests/complexity-routing.test.ts +294 -0
- package/dist/resources/extensions/gsd/tests/context-compression.test.ts +180 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +132 -0
- package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +28 -0
- package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/dist/resources/extensions/gsd/tests/routing-history.test.ts +87 -0
- package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +130 -0
- package/dist/resources/extensions/gsd/tests/token-profile.test.ts +263 -0
- package/dist/resources/extensions/gsd/types.ts +28 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/dist/resources/extensions/gsd/worktree.ts +24 -2
- package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
- package/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +493 -13
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +422 -62
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-shared.d.ts +12 -0
- package/packages/pi-ai/dist/providers/google-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google-shared.js +9 -22
- package/packages/pi-ai/dist/providers/google-shared.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-shared.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/google-shared.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/google-shared.test.js +125 -0
- package/packages/pi-ai/dist/providers/google-shared.test.js.map +1 -0
- package/packages/pi-ai/src/models.generated.ts +422 -62
- package/packages/pi-ai/src/providers/google-shared.test.ts +137 -0
- package/packages/pi-ai/src/providers/google-shared.ts +10 -19
- package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +21 -0
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
- package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
- package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts +7 -7
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +209 -13
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +67 -0
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/index.d.ts +5 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +4 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +17 -2
- package/packages/pi-coding-agent/dist/main.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +10 -0
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
- package/packages/pi-coding-agent/src/cli/args.ts +21 -0
- package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
- package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
- package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
- package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
- package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
- package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
- package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
- package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +85 -0
- package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +245 -17
- package/packages/pi-coding-agent/src/index.ts +5 -0
- package/packages/pi-coding-agent/src/main.ts +19 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
- package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +13 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/pkg/dist/modes/interactive/theme/theme.js +10 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
- package/src/resources/extensions/gsd/activity-log.ts +37 -7
- package/src/resources/extensions/gsd/auto-dashboard.ts +4 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +9 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +91 -42
- package/src/resources/extensions/gsd/auto-recovery.ts +7 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/src/resources/extensions/gsd/auto.ts +177 -25
- package/src/resources/extensions/gsd/commands.ts +264 -23
- package/src/resources/extensions/gsd/complexity.ts +236 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/src/resources/extensions/gsd/docs/preferences-reference.md +202 -2
- package/src/resources/extensions/gsd/files.ts +129 -3
- package/src/resources/extensions/gsd/git-service.ts +19 -8
- package/src/resources/extensions/gsd/gitignore.ts +41 -2
- package/src/resources/extensions/gsd/guided-flow.ts +247 -10
- package/src/resources/extensions/gsd/index.ts +47 -3
- package/src/resources/extensions/gsd/metrics.ts +44 -0
- package/src/resources/extensions/gsd/native-git-bridge.ts +5 -0
- package/src/resources/extensions/gsd/native-parser-bridge.ts +5 -0
- package/src/resources/extensions/gsd/paths.ts +9 -0
- package/src/resources/extensions/gsd/preferences.ts +181 -2
- package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/src/resources/extensions/gsd/prompts/system.md +2 -0
- package/src/resources/extensions/gsd/queue-order.ts +231 -0
- package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/src/resources/extensions/gsd/routing-history.ts +290 -0
- package/src/resources/extensions/gsd/state.ts +15 -3
- package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/src/resources/extensions/gsd/templates/preferences.md +14 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/budget-prediction.test.ts +220 -0
- package/src/resources/extensions/gsd/tests/complexity-routing.test.ts +294 -0
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +132 -0
- package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +28 -0
- package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/src/resources/extensions/gsd/tests/routing-history.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +130 -0
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +263 -0
- package/src/resources/extensions/gsd/types.ts +28 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/src/resources/extensions/gsd/worktree.ts +24 -2
- package/src/resources/extensions/shared/next-action-ui.ts +16 -1
|
@@ -12,7 +12,8 @@ import { fileURLToPath } from "node:url";
|
|
|
12
12
|
import { deriveState } from "./state.js";
|
|
13
13
|
import { GSDDashboardOverlay } from "./dashboard-overlay.js";
|
|
14
14
|
import { showQueue, showDiscuss } from "./guided-flow.js";
|
|
15
|
-
import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode } from "./auto.js";
|
|
15
|
+
import { startAuto, stopAuto, pauseAuto, isAutoActive, isAutoPaused, isStepMode, stopAutoRemote } from "./auto.js";
|
|
16
|
+
import { resolveProjectRoot } from "./worktree.js";
|
|
16
17
|
import {
|
|
17
18
|
getGlobalGSDPreferencesPath,
|
|
18
19
|
getLegacyGlobalGSDPreferencesPath,
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
loadEffectiveGSDPreferences,
|
|
23
24
|
resolveAllSkillReferences,
|
|
24
25
|
} from "./preferences.js";
|
|
25
|
-
import { loadFile, saveFile, appendOverride } from "./files.js";
|
|
26
|
+
import { loadFile, saveFile, appendOverride, appendKnowledge } from "./files.js";
|
|
26
27
|
import {
|
|
27
28
|
formatDoctorIssuesForPrompt,
|
|
28
29
|
formatDoctorReport,
|
|
@@ -56,14 +57,19 @@ function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportT
|
|
|
56
57
|
);
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
/** Resolve the effective project root, accounting for worktree paths. */
|
|
61
|
+
function projectRoot(): string {
|
|
62
|
+
return resolveProjectRoot(process.cwd());
|
|
63
|
+
}
|
|
64
|
+
|
|
59
65
|
export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
60
66
|
pi.registerCommand("gsd", {
|
|
61
|
-
description: "GSD — Get Shit Done: /gsd next|auto|stop|pause|status|queue|history|undo|skip|export|cleanup|prefs|config|hooks|doctor|migrate|remote|steer",
|
|
67
|
+
description: "GSD — Get Shit Done: /gsd next|auto|stop|pause|status|queue|history|undo|skip|export|cleanup|prefs|config|hooks|doctor|migrate|remote|steer|knowledge",
|
|
62
68
|
getArgumentCompletions: (prefix: string) => {
|
|
63
69
|
const subcommands = [
|
|
64
70
|
"next", "auto", "stop", "pause", "status", "queue", "discuss",
|
|
65
71
|
"history", "undo", "skip", "export", "cleanup", "prefs",
|
|
66
|
-
"config", "hooks", "doctor", "migrate", "remote", "steer",
|
|
72
|
+
"config", "hooks", "doctor", "migrate", "remote", "steer", "knowledge",
|
|
67
73
|
];
|
|
68
74
|
const parts = prefix.trim().split(/\s+/);
|
|
69
75
|
|
|
@@ -126,6 +132,13 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
126
132
|
.map((cmd) => ({ value: `cleanup ${cmd}`, label: cmd }));
|
|
127
133
|
}
|
|
128
134
|
|
|
135
|
+
if (parts[0] === "knowledge" && parts.length <= 2) {
|
|
136
|
+
const subPrefix = parts[1] ?? "";
|
|
137
|
+
return ["rule", "pattern", "lesson"]
|
|
138
|
+
.filter((cmd) => cmd.startsWith(subPrefix))
|
|
139
|
+
.map((cmd) => ({ value: `knowledge ${cmd}`, label: cmd }));
|
|
140
|
+
}
|
|
141
|
+
|
|
129
142
|
if (parts[0] === "doctor") {
|
|
130
143
|
const modePrefix = parts[1] ?? "";
|
|
131
144
|
const modes = ["fix", "heal", "audit"];
|
|
@@ -162,23 +175,31 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
162
175
|
|
|
163
176
|
if (trimmed === "next" || trimmed.startsWith("next ")) {
|
|
164
177
|
if (trimmed.includes("--dry-run")) {
|
|
165
|
-
await handleDryRun(ctx,
|
|
178
|
+
await handleDryRun(ctx, projectRoot());
|
|
166
179
|
return;
|
|
167
180
|
}
|
|
168
181
|
const verboseMode = trimmed.includes("--verbose");
|
|
169
|
-
await startAuto(ctx, pi,
|
|
182
|
+
await startAuto(ctx, pi, projectRoot(), verboseMode, { step: true });
|
|
170
183
|
return;
|
|
171
184
|
}
|
|
172
185
|
|
|
173
186
|
if (trimmed === "auto" || trimmed.startsWith("auto ")) {
|
|
174
187
|
const verboseMode = trimmed.includes("--verbose");
|
|
175
|
-
await startAuto(ctx, pi,
|
|
188
|
+
await startAuto(ctx, pi, projectRoot(), verboseMode);
|
|
176
189
|
return;
|
|
177
190
|
}
|
|
178
191
|
|
|
179
192
|
if (trimmed === "stop") {
|
|
180
193
|
if (!isAutoActive() && !isAutoPaused()) {
|
|
181
|
-
|
|
194
|
+
// Not running in this process — check for a remote auto-mode session
|
|
195
|
+
const result = stopAutoRemote(projectRoot());
|
|
196
|
+
if (result.found) {
|
|
197
|
+
ctx.ui.notify(`Sent stop signal to auto-mode session (PID ${result.pid}). It will shut down gracefully.`, "info");
|
|
198
|
+
} else if (result.error) {
|
|
199
|
+
ctx.ui.notify(`Failed to stop remote auto-mode: ${result.error}`, "error");
|
|
200
|
+
} else {
|
|
201
|
+
ctx.ui.notify("Auto-mode is not running.", "info");
|
|
202
|
+
}
|
|
182
203
|
return;
|
|
183
204
|
}
|
|
184
205
|
await stopAuto(ctx, pi);
|
|
@@ -199,42 +220,42 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
199
220
|
}
|
|
200
221
|
|
|
201
222
|
if (trimmed === "history" || trimmed.startsWith("history ")) {
|
|
202
|
-
await handleHistory(trimmed.replace(/^history\s*/, "").trim(), ctx,
|
|
223
|
+
await handleHistory(trimmed.replace(/^history\s*/, "").trim(), ctx, projectRoot());
|
|
203
224
|
return;
|
|
204
225
|
}
|
|
205
226
|
|
|
206
227
|
if (trimmed === "undo" || trimmed.startsWith("undo ")) {
|
|
207
|
-
await handleUndo(trimmed.replace(/^undo\s*/, "").trim(), ctx, pi,
|
|
228
|
+
await handleUndo(trimmed.replace(/^undo\s*/, "").trim(), ctx, pi, projectRoot());
|
|
208
229
|
return;
|
|
209
230
|
}
|
|
210
231
|
|
|
211
232
|
if (trimmed.startsWith("skip ")) {
|
|
212
|
-
await handleSkip(trimmed.replace(/^skip\s*/, "").trim(), ctx,
|
|
233
|
+
await handleSkip(trimmed.replace(/^skip\s*/, "").trim(), ctx, projectRoot());
|
|
213
234
|
return;
|
|
214
235
|
}
|
|
215
236
|
|
|
216
237
|
if (trimmed === "export" || trimmed.startsWith("export ")) {
|
|
217
|
-
await handleExport(trimmed.replace(/^export\s*/, "").trim(), ctx,
|
|
238
|
+
await handleExport(trimmed.replace(/^export\s*/, "").trim(), ctx, projectRoot());
|
|
218
239
|
return;
|
|
219
240
|
}
|
|
220
241
|
|
|
221
242
|
if (trimmed === "cleanup branches") {
|
|
222
|
-
await handleCleanupBranches(ctx,
|
|
243
|
+
await handleCleanupBranches(ctx, projectRoot());
|
|
223
244
|
return;
|
|
224
245
|
}
|
|
225
246
|
|
|
226
247
|
if (trimmed === "cleanup snapshots") {
|
|
227
|
-
await handleCleanupSnapshots(ctx,
|
|
248
|
+
await handleCleanupSnapshots(ctx, projectRoot());
|
|
228
249
|
return;
|
|
229
250
|
}
|
|
230
251
|
|
|
231
252
|
if (trimmed === "queue") {
|
|
232
|
-
await showQueue(ctx, pi,
|
|
253
|
+
await showQueue(ctx, pi, projectRoot());
|
|
233
254
|
return;
|
|
234
255
|
}
|
|
235
256
|
|
|
236
257
|
if (trimmed === "discuss") {
|
|
237
|
-
await showDiscuss(ctx, pi,
|
|
258
|
+
await showDiscuss(ctx, pi, projectRoot());
|
|
238
259
|
return;
|
|
239
260
|
}
|
|
240
261
|
|
|
@@ -258,6 +279,15 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
258
279
|
return;
|
|
259
280
|
}
|
|
260
281
|
|
|
282
|
+
if (trimmed.startsWith("knowledge ")) {
|
|
283
|
+
await handleKnowledge(trimmed.replace(/^knowledge\s+/, "").trim(), ctx);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (trimmed === "knowledge") {
|
|
287
|
+
ctx.ui.notify("Usage: /gsd knowledge <rule|pattern|lesson> <description>. Example: /gsd knowledge rule Use real DB for integration tests", "warning");
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
261
291
|
if (trimmed === "migrate" || trimmed.startsWith("migrate ")) {
|
|
262
292
|
const { handleMigrate } = await import("./migrate/command.js");
|
|
263
293
|
await handleMigrate(trimmed.replace(/^migrate\s*/, "").trim(), ctx, pi);
|
|
@@ -271,12 +301,12 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
271
301
|
|
|
272
302
|
if (trimmed === "") {
|
|
273
303
|
// Bare /gsd defaults to step mode
|
|
274
|
-
await startAuto(ctx, pi,
|
|
304
|
+
await startAuto(ctx, pi, projectRoot(), false, { step: true });
|
|
275
305
|
return;
|
|
276
306
|
}
|
|
277
307
|
|
|
278
308
|
ctx.ui.notify(
|
|
279
|
-
`Unknown: /gsd ${trimmed}. Use /gsd next|auto|stop|pause|status|queue|discuss|history|undo|skip <unit>|export|cleanup|prefs|config|hooks|doctor|migrate|remote|steer <change>.`,
|
|
309
|
+
`Unknown: /gsd ${trimmed}. Use /gsd next|auto|stop|pause|status|queue|discuss|history|undo|skip <unit>|export|cleanup|prefs|config|hooks|doctor|migrate|remote|steer <change>|knowledge <type> <entry>.`,
|
|
280
310
|
"warning",
|
|
281
311
|
);
|
|
282
312
|
},
|
|
@@ -284,7 +314,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
284
314
|
}
|
|
285
315
|
|
|
286
316
|
async function handleStatus(ctx: ExtensionCommandContext): Promise<void> {
|
|
287
|
-
const basePath =
|
|
317
|
+
const basePath = projectRoot();
|
|
288
318
|
const state = await deriveState(basePath);
|
|
289
319
|
|
|
290
320
|
if (state.registry.length === 0) {
|
|
@@ -368,9 +398,9 @@ async function handleDoctor(args: string, ctx: ExtensionCommandContext, pi: Exte
|
|
|
368
398
|
const parts = trimmed ? trimmed.split(/\s+/) : [];
|
|
369
399
|
const mode = parts[0] === "fix" || parts[0] === "heal" || parts[0] === "audit" ? parts[0] : "doctor";
|
|
370
400
|
const requestedScope = mode === "doctor" ? parts[0] : parts[1];
|
|
371
|
-
const scope = await selectDoctorScope(
|
|
401
|
+
const scope = await selectDoctorScope(projectRoot(), requestedScope);
|
|
372
402
|
const effectiveScope = mode === "audit" ? requestedScope : scope;
|
|
373
|
-
const report = await runGSDDoctor(
|
|
403
|
+
const report = await runGSDDoctor(projectRoot(), {
|
|
374
404
|
fix: mode === "fix" || mode === "heal",
|
|
375
405
|
scope: effectiveScope,
|
|
376
406
|
});
|
|
@@ -487,8 +517,10 @@ async function handlePrefsWizard(
|
|
|
487
517
|
prefs.auto_supervisor = autoSup;
|
|
488
518
|
}
|
|
489
519
|
|
|
490
|
-
// ─── Git
|
|
520
|
+
// ─── Git settings ───────────────────────────────────────────────────────
|
|
491
521
|
const git: Record<string, unknown> = (prefs.git as Record<string, unknown>) ?? {};
|
|
522
|
+
|
|
523
|
+
// main_branch
|
|
492
524
|
const currentBranch = git.main_branch ? String(git.main_branch) : "";
|
|
493
525
|
const branchInput = await ctx.ui.input(
|
|
494
526
|
`Git main branch${currentBranch ? ` (current: ${currentBranch})` : ""}:`,
|
|
@@ -502,6 +534,100 @@ async function handlePrefsWizard(
|
|
|
502
534
|
delete git.main_branch;
|
|
503
535
|
}
|
|
504
536
|
}
|
|
537
|
+
|
|
538
|
+
// Boolean git toggles
|
|
539
|
+
const gitBooleanFields = [
|
|
540
|
+
{ key: "auto_push", label: "Auto-push commits after committing", defaultVal: false },
|
|
541
|
+
{ key: "push_branches", label: "Push milestone branches to remote", defaultVal: false },
|
|
542
|
+
{ key: "snapshots", label: "Create WIP snapshot commits during long tasks", defaultVal: false },
|
|
543
|
+
] as const;
|
|
544
|
+
|
|
545
|
+
for (const field of gitBooleanFields) {
|
|
546
|
+
const current = git[field.key];
|
|
547
|
+
const currentStr = current !== undefined ? String(current) : "";
|
|
548
|
+
const choice = await ctx.ui.select(
|
|
549
|
+
`${field.label}${currentStr ? ` (current: ${currentStr})` : ` (default: ${field.defaultVal})`}:`,
|
|
550
|
+
["true", "false", "(keep current)"],
|
|
551
|
+
);
|
|
552
|
+
if (choice && choice !== "(keep current)") {
|
|
553
|
+
git[field.key] = choice === "true";
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// remote
|
|
558
|
+
const currentRemote = git.remote ? String(git.remote) : "";
|
|
559
|
+
const remoteInput = await ctx.ui.input(
|
|
560
|
+
`Git remote name${currentRemote ? ` (current: ${currentRemote})` : " (default: origin)"}:`,
|
|
561
|
+
currentRemote || "origin",
|
|
562
|
+
);
|
|
563
|
+
if (remoteInput !== null && remoteInput !== undefined) {
|
|
564
|
+
const val = remoteInput.trim();
|
|
565
|
+
if (val && val !== "origin") {
|
|
566
|
+
git.remote = val;
|
|
567
|
+
} else if (!val && currentRemote) {
|
|
568
|
+
delete git.remote;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// pre_merge_check
|
|
573
|
+
const currentPreMerge = git.pre_merge_check !== undefined ? String(git.pre_merge_check) : "";
|
|
574
|
+
const preMergeChoice = await ctx.ui.select(
|
|
575
|
+
`Pre-merge check${currentPreMerge ? ` (current: ${currentPreMerge})` : " (default: false)"}:`,
|
|
576
|
+
["true", "false", "auto", "(keep current)"],
|
|
577
|
+
);
|
|
578
|
+
if (preMergeChoice && preMergeChoice !== "(keep current)") {
|
|
579
|
+
if (preMergeChoice === "auto") {
|
|
580
|
+
git.pre_merge_check = "auto";
|
|
581
|
+
} else {
|
|
582
|
+
git.pre_merge_check = preMergeChoice === "true";
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// commit_type
|
|
587
|
+
const currentCommitType = git.commit_type ? String(git.commit_type) : "";
|
|
588
|
+
const commitTypes = ["feat", "fix", "refactor", "docs", "test", "chore", "perf", "ci", "build", "style", "(inferred — default)", "(keep current)"];
|
|
589
|
+
const commitChoice = await ctx.ui.select(
|
|
590
|
+
`Default commit type${currentCommitType ? ` (current: ${currentCommitType})` : ""}:`,
|
|
591
|
+
commitTypes,
|
|
592
|
+
);
|
|
593
|
+
if (commitChoice && typeof commitChoice === "string" && commitChoice !== "(keep current)") {
|
|
594
|
+
if ((commitChoice as string).startsWith("(inferred")) {
|
|
595
|
+
delete git.commit_type;
|
|
596
|
+
} else {
|
|
597
|
+
git.commit_type = commitChoice;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// merge_strategy
|
|
602
|
+
const currentMerge = git.merge_strategy ? String(git.merge_strategy) : "";
|
|
603
|
+
const mergeChoice = await ctx.ui.select(
|
|
604
|
+
`Merge strategy${currentMerge ? ` (current: ${currentMerge})` : ""}:`,
|
|
605
|
+
["squash", "merge", "(keep current)"],
|
|
606
|
+
);
|
|
607
|
+
if (mergeChoice && mergeChoice !== "(keep current)") {
|
|
608
|
+
git.merge_strategy = mergeChoice;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// isolation
|
|
612
|
+
const currentIsolation = git.isolation ? String(git.isolation) : "";
|
|
613
|
+
const isolationChoice = await ctx.ui.select(
|
|
614
|
+
`Git isolation strategy${currentIsolation ? ` (current: ${currentIsolation})` : " (default: worktree)"}:`,
|
|
615
|
+
["worktree", "branch", "(keep current)"],
|
|
616
|
+
);
|
|
617
|
+
if (isolationChoice && isolationChoice !== "(keep current)") {
|
|
618
|
+
git.isolation = isolationChoice;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ─── Git commit_docs ────────────────────────────────────────────────────
|
|
622
|
+
const currentCommitDocs = git.commit_docs;
|
|
623
|
+
const commitDocsChoice = await ctx.ui.select(
|
|
624
|
+
`Track .gsd/ planning docs in git${currentCommitDocs !== undefined ? ` (current: ${currentCommitDocs})` : ""}:`,
|
|
625
|
+
["true", "false", "(keep current)"],
|
|
626
|
+
);
|
|
627
|
+
if (commitDocsChoice && commitDocsChoice !== "(keep current)") {
|
|
628
|
+
git.commit_docs = commitDocsChoice === "true";
|
|
629
|
+
}
|
|
630
|
+
|
|
505
631
|
if (Object.keys(git).length > 0) {
|
|
506
632
|
prefs.git = git;
|
|
507
633
|
}
|
|
@@ -526,6 +652,89 @@ async function handlePrefsWizard(
|
|
|
526
652
|
prefs.unique_milestone_ids = uniqueChoice === "true";
|
|
527
653
|
}
|
|
528
654
|
|
|
655
|
+
// ─── Budget & cost control ────────────────────────────────────────────
|
|
656
|
+
const currentCeiling = prefs.budget_ceiling;
|
|
657
|
+
const ceilingStr = currentCeiling !== undefined ? String(currentCeiling) : "";
|
|
658
|
+
const ceilingInput = await ctx.ui.input(
|
|
659
|
+
`Budget ceiling (USD)${ceilingStr ? ` (current: $${ceilingStr})` : " (default: no limit)"}:`,
|
|
660
|
+
ceilingStr || "",
|
|
661
|
+
);
|
|
662
|
+
if (ceilingInput !== null && ceilingInput !== undefined) {
|
|
663
|
+
const val = ceilingInput.trim().replace(/^\$/, "");
|
|
664
|
+
if (val && !isNaN(Number(val)) && isFinite(Number(val))) {
|
|
665
|
+
prefs.budget_ceiling = Number(val);
|
|
666
|
+
} else if (val && (isNaN(Number(val)) || !isFinite(Number(val)))) {
|
|
667
|
+
ctx.ui.notify(`Invalid budget ceiling "${val}" — must be a number. Keeping previous value.`, "warning");
|
|
668
|
+
} else if (!val && ceilingStr) {
|
|
669
|
+
delete prefs.budget_ceiling;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const currentEnforcement = (prefs.budget_enforcement as string) ?? "";
|
|
674
|
+
const enforcementChoice = await ctx.ui.select(
|
|
675
|
+
`Budget enforcement${currentEnforcement ? ` (current: ${currentEnforcement})` : " (default: pause)"}:`,
|
|
676
|
+
["warn", "pause", "halt", "(keep current)"],
|
|
677
|
+
);
|
|
678
|
+
if (enforcementChoice && enforcementChoice !== "(keep current)") {
|
|
679
|
+
prefs.budget_enforcement = enforcementChoice;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const currentContextPause = prefs.context_pause_threshold;
|
|
683
|
+
const contextPauseStr = currentContextPause !== undefined ? String(currentContextPause) : "";
|
|
684
|
+
const contextPauseInput = await ctx.ui.input(
|
|
685
|
+
`Context pause threshold (0-100%, 0=disabled)${contextPauseStr ? ` (current: ${contextPauseStr}%)` : " (default: 0)"}:`,
|
|
686
|
+
contextPauseStr || "0",
|
|
687
|
+
);
|
|
688
|
+
if (contextPauseInput !== null && contextPauseInput !== undefined) {
|
|
689
|
+
const val = contextPauseInput.trim().replace(/%$/, "");
|
|
690
|
+
if (val && !isNaN(Number(val)) && Number(val) >= 0 && Number(val) <= 100) {
|
|
691
|
+
const num = Number(val);
|
|
692
|
+
if (num === 0) {
|
|
693
|
+
delete prefs.context_pause_threshold;
|
|
694
|
+
} else {
|
|
695
|
+
prefs.context_pause_threshold = num;
|
|
696
|
+
}
|
|
697
|
+
} else if (val && (isNaN(Number(val)) || Number(val) < 0 || Number(val) > 100)) {
|
|
698
|
+
ctx.ui.notify(`Invalid context pause threshold "${val}" — must be 0-100. Keeping previous value.`, "warning");
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// ─── Notifications ────────────────────────────────────────────────────
|
|
703
|
+
const notif: Record<string, boolean> = (prefs.notifications as Record<string, boolean>) ?? {};
|
|
704
|
+
const notifFields = [
|
|
705
|
+
{ key: "enabled", label: "Notifications enabled (master toggle)", defaultVal: true },
|
|
706
|
+
{ key: "on_complete", label: "Notify on unit completion", defaultVal: true },
|
|
707
|
+
{ key: "on_error", label: "Notify on errors", defaultVal: true },
|
|
708
|
+
{ key: "on_budget", label: "Notify on budget thresholds", defaultVal: true },
|
|
709
|
+
{ key: "on_milestone", label: "Notify on milestone completion", defaultVal: true },
|
|
710
|
+
{ key: "on_attention", label: "Notify when manual attention needed", defaultVal: true },
|
|
711
|
+
] as const;
|
|
712
|
+
|
|
713
|
+
for (const field of notifFields) {
|
|
714
|
+
const current = notif[field.key];
|
|
715
|
+
const currentStr = current !== undefined ? String(current) : "";
|
|
716
|
+
const choice = await ctx.ui.select(
|
|
717
|
+
`${field.label}${currentStr ? ` (current: ${currentStr})` : ` (default: ${field.defaultVal})`}:`,
|
|
718
|
+
["true", "false", "(keep current)"],
|
|
719
|
+
);
|
|
720
|
+
if (choice && choice !== "(keep current)") {
|
|
721
|
+
notif[field.key] = choice === "true";
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (Object.keys(notif).length > 0) {
|
|
725
|
+
prefs.notifications = notif;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// ─── UAT dispatch ─────────────────────────────────────────────────────
|
|
729
|
+
const currentUat = prefs.uat_dispatch;
|
|
730
|
+
const uatChoice = await ctx.ui.select(
|
|
731
|
+
`UAT dispatch mode${currentUat !== undefined ? ` (current: ${currentUat})` : " (default: false)"}:`,
|
|
732
|
+
["true", "false", "(keep current)"],
|
|
733
|
+
);
|
|
734
|
+
if (uatChoice && uatChoice !== "(keep current)") {
|
|
735
|
+
prefs.uat_dispatch = uatChoice === "true";
|
|
736
|
+
}
|
|
737
|
+
|
|
529
738
|
// ─── Serialize to frontmatter ───────────────────────────────────────────
|
|
530
739
|
prefs.version = prefs.version || 1;
|
|
531
740
|
const frontmatter = serializePreferencesToFrontmatter(prefs);
|
|
@@ -616,7 +825,10 @@ function serializePreferencesToFrontmatter(prefs: Record<string, unknown>): stri
|
|
|
616
825
|
const orderedKeys = [
|
|
617
826
|
"version", "always_use_skills", "prefer_skills", "avoid_skills",
|
|
618
827
|
"skill_rules", "custom_instructions", "models", "skill_discovery",
|
|
619
|
-
"auto_supervisor", "uat_dispatch", "unique_milestone_ids",
|
|
828
|
+
"auto_supervisor", "uat_dispatch", "unique_milestone_ids",
|
|
829
|
+
"budget_ceiling", "budget_enforcement", "context_pause_threshold",
|
|
830
|
+
"notifications", "remote_questions", "git",
|
|
831
|
+
"post_unit_hooks", "pre_dispatch_hooks",
|
|
620
832
|
];
|
|
621
833
|
|
|
622
834
|
const seen = new Set<string>();
|
|
@@ -954,6 +1166,35 @@ async function handleCleanupSnapshots(ctx: ExtensionCommandContext, basePath: st
|
|
|
954
1166
|
ctx.ui.notify(`Pruned ${pruned} old snapshot refs. ${refs.length - pruned} remain.`, "success");
|
|
955
1167
|
}
|
|
956
1168
|
|
|
1169
|
+
async function handleKnowledge(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
1170
|
+
const parts = args.split(/\s+/);
|
|
1171
|
+
const typeArg = parts[0]?.toLowerCase();
|
|
1172
|
+
|
|
1173
|
+
if (!typeArg || !["rule", "pattern", "lesson"].includes(typeArg)) {
|
|
1174
|
+
ctx.ui.notify(
|
|
1175
|
+
"Usage: /gsd knowledge <rule|pattern|lesson> <description>\nExample: /gsd knowledge rule Use real DB for integration tests",
|
|
1176
|
+
"warning",
|
|
1177
|
+
);
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
const entryText = parts.slice(1).join(" ").trim();
|
|
1182
|
+
if (!entryText) {
|
|
1183
|
+
ctx.ui.notify(`Usage: /gsd knowledge ${typeArg} <description>`, "warning");
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const type = typeArg as "rule" | "pattern" | "lesson";
|
|
1188
|
+
const basePath = process.cwd();
|
|
1189
|
+
const state = await deriveState(basePath);
|
|
1190
|
+
const scope = state.activeMilestone?.id
|
|
1191
|
+
? `${state.activeMilestone.id}${state.activeSlice ? `/${state.activeSlice.id}` : ""}`
|
|
1192
|
+
: "global";
|
|
1193
|
+
|
|
1194
|
+
await appendKnowledge(basePath, type, entryText, scope);
|
|
1195
|
+
ctx.ui.notify(`Added ${type} to KNOWLEDGE.md: "${entryText}"`, "success");
|
|
1196
|
+
}
|
|
1197
|
+
|
|
957
1198
|
async function handleSteer(change: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
|
|
958
1199
|
const basePath = process.cwd();
|
|
959
1200
|
const state = await deriveState(basePath);
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Task Complexity Classification
|
|
3
|
+
*
|
|
4
|
+
* Classifies task plans and unit types by complexity to enable model routing.
|
|
5
|
+
* Pure heuristics + adaptive learning — no LLM calls, sub-millisecond.
|
|
6
|
+
*
|
|
7
|
+
* Combined approach:
|
|
8
|
+
* - Task plan analysis (step count, file count, description length, signal words)
|
|
9
|
+
* - Unit type defaults (complete-slice → light, replan → heavy, etc.)
|
|
10
|
+
* - Budget pressure thresholds (50/75/90% graduated downgrade)
|
|
11
|
+
* - Adaptive learning via routing-history (optional)
|
|
12
|
+
*
|
|
13
|
+
* Classification output uses our TokenProfile-aligned TaskComplexity type
|
|
14
|
+
* for the simple classifier, and ComplexityTier for the full unit classifier.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import type { ComplexityTier, ClassificationResult, TaskMetadata } from "./types.js";
|
|
20
|
+
|
|
21
|
+
// Re-export for convenience
|
|
22
|
+
export type { ComplexityTier, ClassificationResult, TaskMetadata };
|
|
23
|
+
|
|
24
|
+
// ─── Simple Task Complexity (for task plan analysis) ──────────────────────
|
|
25
|
+
|
|
26
|
+
export type TaskComplexity = "simple" | "standard" | "complex";
|
|
27
|
+
|
|
28
|
+
/** Words that signal non-trivial work requiring full reasoning capacity */
|
|
29
|
+
const COMPLEXITY_SIGNALS = [
|
|
30
|
+
"research", "investigate", "refactor", "migrate", "integrate",
|
|
31
|
+
"complex", "architect", "redesign", "security", "performance",
|
|
32
|
+
"concurrent", "parallel", "distributed", "backward.?compat",
|
|
33
|
+
"migration", "architecture", "concurrency", "compatibility",
|
|
34
|
+
];
|
|
35
|
+
const COMPLEXITY_PATTERN = new RegExp(COMPLEXITY_SIGNALS.join("|"), "i");
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Classify a task plan by its structural complexity.
|
|
39
|
+
* Used by dispatch to select execution_simple vs execution model.
|
|
40
|
+
*/
|
|
41
|
+
export function classifyTaskComplexity(planContent: string): TaskComplexity {
|
|
42
|
+
if (!planContent || planContent.trim().length === 0) return "standard";
|
|
43
|
+
|
|
44
|
+
const stepsMatch = planContent.match(/##\s*Steps\s*\n([\s\S]*?)(?=\n##|\n---|$)/i);
|
|
45
|
+
const stepsSection = stepsMatch?.[1] ?? "";
|
|
46
|
+
const stepCount = (stepsSection.match(/^\s*\d+\.\s/gm) ?? []).length;
|
|
47
|
+
|
|
48
|
+
if (!stepsMatch) return "standard";
|
|
49
|
+
|
|
50
|
+
const stepsIdx = planContent.search(/##\s*Steps/i);
|
|
51
|
+
const descriptionLength = stepsIdx > 0 ? planContent.slice(0, stepsIdx).length : planContent.length;
|
|
52
|
+
|
|
53
|
+
const filePatterns = planContent.match(/`[a-zA-Z0-9_/.-]+\.[a-z]{1,4}`/g) ?? [];
|
|
54
|
+
const uniqueFiles = new Set(filePatterns.map(f => f.replace(/`/g, "")));
|
|
55
|
+
const fileCount = uniqueFiles.size;
|
|
56
|
+
|
|
57
|
+
const hasComplexitySignals = COMPLEXITY_PATTERN.test(planContent);
|
|
58
|
+
|
|
59
|
+
// Count fenced code blocks (from #579 Phase 4)
|
|
60
|
+
const codeBlockCount = (planContent.match(/^```/gm) ?? []).length / 2;
|
|
61
|
+
|
|
62
|
+
if (stepCount >= 8 || fileCount >= 8 || descriptionLength > 2000 || codeBlockCount >= 5) {
|
|
63
|
+
return "complex";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (stepCount <= 3 && descriptionLength < 500 && fileCount <= 3 && !hasComplexitySignals) {
|
|
67
|
+
return "simple";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return "standard";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── Unit Type → Default Tier Mapping (from #579) ─────────────────────────
|
|
74
|
+
|
|
75
|
+
const UNIT_TYPE_TIERS: Record<string, ComplexityTier> = {
|
|
76
|
+
// Light: structured summaries, completion, UAT
|
|
77
|
+
"complete-slice": "light",
|
|
78
|
+
"run-uat": "light",
|
|
79
|
+
|
|
80
|
+
// Standard: research, routine planning
|
|
81
|
+
"research-milestone": "standard",
|
|
82
|
+
"research-slice": "standard",
|
|
83
|
+
"plan-milestone": "standard",
|
|
84
|
+
"plan-slice": "standard",
|
|
85
|
+
|
|
86
|
+
// Heavy: execution default (upgraded by metadata), replanning
|
|
87
|
+
"execute-task": "standard",
|
|
88
|
+
"replan-slice": "heavy",
|
|
89
|
+
"reassess-roadmap": "heavy",
|
|
90
|
+
"complete-milestone": "standard",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Classify unit complexity for model routing.
|
|
95
|
+
* Uses unit type defaults, task metadata analysis, and budget pressure.
|
|
96
|
+
*
|
|
97
|
+
* @param unitType The type of unit being dispatched
|
|
98
|
+
* @param unitId The unit ID (e.g. "M001/S01/T01")
|
|
99
|
+
* @param basePath Project base path (for reading task plans)
|
|
100
|
+
* @param budgetPct Current budget usage as fraction (0.0-1.0+), or undefined
|
|
101
|
+
* @param metadata Optional pre-parsed task metadata
|
|
102
|
+
*/
|
|
103
|
+
export function classifyUnitComplexity(
|
|
104
|
+
unitType: string,
|
|
105
|
+
unitId: string,
|
|
106
|
+
basePath: string,
|
|
107
|
+
budgetPct?: number,
|
|
108
|
+
metadata?: TaskMetadata,
|
|
109
|
+
): ClassificationResult {
|
|
110
|
+
// Hook units default to light
|
|
111
|
+
if (unitType.startsWith("hook/")) {
|
|
112
|
+
return applyBudgetPressure({ tier: "light", reason: "hook unit", downgraded: false }, budgetPct);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Triage/capture units default to light
|
|
116
|
+
if (unitType === "triage-captures" || unitType.startsWith("quick-task")) {
|
|
117
|
+
return applyBudgetPressure({ tier: "light", reason: `${unitType} unit`, downgraded: false }, budgetPct);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let tier = UNIT_TYPE_TIERS[unitType] ?? "standard";
|
|
121
|
+
let reason = `unit type: ${unitType}`;
|
|
122
|
+
|
|
123
|
+
// For execute-task, analyze task metadata for complexity signals
|
|
124
|
+
if (unitType === "execute-task") {
|
|
125
|
+
const analysis = analyzeTaskFromPlan(unitId, basePath, metadata);
|
|
126
|
+
if (analysis) {
|
|
127
|
+
tier = analysis.tier;
|
|
128
|
+
reason = analysis.reason;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return applyBudgetPressure({ tier, reason, downgraded: false }, budgetPct);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Tier Helpers ─────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
export function tierLabel(tier: ComplexityTier): string {
|
|
138
|
+
switch (tier) {
|
|
139
|
+
case "light": return "L";
|
|
140
|
+
case "standard": return "S";
|
|
141
|
+
case "heavy": return "H";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function tierOrdinal(tier: ComplexityTier): number {
|
|
146
|
+
switch (tier) {
|
|
147
|
+
case "light": return 0;
|
|
148
|
+
case "standard": return 1;
|
|
149
|
+
case "heavy": return 2;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function escalateTier(currentTier: ComplexityTier): ComplexityTier | null {
|
|
154
|
+
switch (currentTier) {
|
|
155
|
+
case "light": return "standard";
|
|
156
|
+
case "standard": return "heavy";
|
|
157
|
+
case "heavy": return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ─── Budget Pressure (from #579 — graduated thresholds) ───────────────────
|
|
162
|
+
|
|
163
|
+
function applyBudgetPressure(
|
|
164
|
+
result: ClassificationResult,
|
|
165
|
+
budgetPct?: number,
|
|
166
|
+
): ClassificationResult {
|
|
167
|
+
if (budgetPct === undefined || budgetPct < 0.5) return result;
|
|
168
|
+
|
|
169
|
+
const original = result.tier;
|
|
170
|
+
|
|
171
|
+
if (budgetPct >= 0.9) {
|
|
172
|
+
// >90%: almost everything goes to light
|
|
173
|
+
if (result.tier !== "heavy") {
|
|
174
|
+
result.tier = "light";
|
|
175
|
+
} else {
|
|
176
|
+
result.tier = "standard";
|
|
177
|
+
}
|
|
178
|
+
} else if (budgetPct >= 0.75) {
|
|
179
|
+
// 75-90%: only heavy stays, standard → light
|
|
180
|
+
if (result.tier === "standard") {
|
|
181
|
+
result.tier = "light";
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
// 50-75%: standard → light
|
|
185
|
+
if (result.tier === "standard") {
|
|
186
|
+
result.tier = "light";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (result.tier !== original) {
|
|
191
|
+
result.downgraded = true;
|
|
192
|
+
result.reason = `${result.reason} (budget pressure: ${Math.round(budgetPct * 100)}%)`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ─── Task Plan Analysis ───────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
interface TaskAnalysis {
|
|
201
|
+
tier: ComplexityTier;
|
|
202
|
+
reason: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function analyzeTaskFromPlan(
|
|
206
|
+
unitId: string,
|
|
207
|
+
basePath: string,
|
|
208
|
+
metadata?: TaskMetadata,
|
|
209
|
+
): TaskAnalysis | null {
|
|
210
|
+
// Try to read the task plan for analysis
|
|
211
|
+
const parts = unitId.split("/");
|
|
212
|
+
if (parts.length < 3) return null;
|
|
213
|
+
|
|
214
|
+
const [mid, sid, tid] = parts;
|
|
215
|
+
const planPath = join(basePath, ".gsd", "milestones", mid, "slices", sid, "tasks", `${tid}-PLAN.md`);
|
|
216
|
+
|
|
217
|
+
let planContent = "";
|
|
218
|
+
try {
|
|
219
|
+
if (existsSync(planPath)) {
|
|
220
|
+
planContent = readFileSync(planPath, "utf-8");
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!planContent) return null;
|
|
227
|
+
|
|
228
|
+
const taskComplexity = classifyTaskComplexity(planContent);
|
|
229
|
+
|
|
230
|
+
// Map TaskComplexity to ComplexityTier
|
|
231
|
+
switch (taskComplexity) {
|
|
232
|
+
case "simple": return { tier: "light", reason: "task plan: simple (few steps, small scope)" };
|
|
233
|
+
case "complex": return { tier: "heavy", reason: "task plan: complex (many steps/files or signal words)" };
|
|
234
|
+
default: return { tier: "standard", reason: "task plan: standard complexity" };
|
|
235
|
+
}
|
|
236
|
+
}
|