gsd-pi 2.16.0 → 2.17.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/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 +71 -41
- package/dist/resources/extensions/gsd/auto-recovery.ts +7 -2
- package/dist/resources/extensions/gsd/auto.ts +54 -15
- package/dist/resources/extensions/gsd/commands.ts +20 -2
- package/dist/resources/extensions/gsd/complexity.ts +236 -0
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -0
- package/dist/resources/extensions/gsd/files.ts +6 -2
- 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 +10 -6
- 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/preferences.ts +122 -1
- package/dist/resources/extensions/gsd/routing-history.ts +290 -0
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -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/git-service.test.ts +132 -0
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +28 -0
- package/dist/resources/extensions/gsd/tests/routing-history.test.ts +87 -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.ts +2 -2
- 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/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/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/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/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/auto-dashboard.ts +4 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +9 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +71 -41
- package/src/resources/extensions/gsd/auto-recovery.ts +7 -2
- package/src/resources/extensions/gsd/auto.ts +54 -15
- package/src/resources/extensions/gsd/commands.ts +20 -2
- package/src/resources/extensions/gsd/complexity.ts +236 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -0
- package/src/resources/extensions/gsd/files.ts +6 -2
- 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 +10 -6
- 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/preferences.ts +122 -1
- package/src/resources/extensions/gsd/routing-history.ts +290 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -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/git-service.test.ts +132 -0
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +28 -0
- package/src/resources/extensions/gsd/tests/routing-history.test.ts +87 -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.ts +2 -2
|
@@ -293,6 +293,41 @@ export function isAutoPaused(): boolean {
|
|
|
293
293
|
return paused;
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Return the base path to use for the auto.lock file.
|
|
298
|
+
* Always uses the original project root (not the worktree) so that
|
|
299
|
+
* a second terminal can discover and stop a running auto-mode session.
|
|
300
|
+
*/
|
|
301
|
+
function lockBase(): string {
|
|
302
|
+
return originalBasePath || basePath;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Attempt to stop a running auto-mode session from a different process.
|
|
307
|
+
* Reads the lock file at the project root, checks if the PID is alive,
|
|
308
|
+
* and sends SIGTERM to gracefully stop it.
|
|
309
|
+
*
|
|
310
|
+
* Returns true if a remote session was found and signaled, false otherwise.
|
|
311
|
+
*/
|
|
312
|
+
export function stopAutoRemote(projectRoot: string): { found: boolean; pid?: number; error?: string } {
|
|
313
|
+
const lock = readCrashLock(projectRoot);
|
|
314
|
+
if (!lock) return { found: false };
|
|
315
|
+
|
|
316
|
+
if (!isLockProcessAlive(lock)) {
|
|
317
|
+
// Stale lock — clean it up
|
|
318
|
+
clearLock(projectRoot);
|
|
319
|
+
return { found: false };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Send SIGTERM — the auto-mode process has a handler that clears the lock and exits
|
|
323
|
+
try {
|
|
324
|
+
process.kill(lock.pid, "SIGTERM");
|
|
325
|
+
return { found: true, pid: lock.pid };
|
|
326
|
+
} catch (err) {
|
|
327
|
+
return { found: false, error: (err as Error).message };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
296
331
|
export function isStepMode(): boolean {
|
|
297
332
|
return stepMode;
|
|
298
333
|
}
|
|
@@ -371,7 +406,7 @@ function startDispatchGapWatchdog(ctx: ExtensionContext, pi: ExtensionAPI): void
|
|
|
371
406
|
export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promise<void> {
|
|
372
407
|
if (!active && !paused) return;
|
|
373
408
|
clearUnitTimeout();
|
|
374
|
-
if (
|
|
409
|
+
if (lockBase()) clearLock(lockBase());
|
|
375
410
|
clearSkillSnapshot();
|
|
376
411
|
_dispatching = false;
|
|
377
412
|
_skipDepth = 0;
|
|
@@ -454,7 +489,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
454
489
|
export async function pauseAuto(ctx?: ExtensionContext, _pi?: ExtensionAPI): Promise<void> {
|
|
455
490
|
if (!active) return;
|
|
456
491
|
clearUnitTimeout();
|
|
457
|
-
if (
|
|
492
|
+
if (lockBase()) clearLock(lockBase());
|
|
458
493
|
|
|
459
494
|
// Remove SIGTERM handler registered at auto-mode start
|
|
460
495
|
deregisterSigtermHandler();
|
|
@@ -527,8 +562,8 @@ export async function startAuto(
|
|
|
527
562
|
}
|
|
528
563
|
}
|
|
529
564
|
|
|
530
|
-
// Re-register SIGTERM handler for the resumed session
|
|
531
|
-
registerSigtermHandler(
|
|
565
|
+
// Re-register SIGTERM handler for the resumed session (use original base for lock)
|
|
566
|
+
registerSigtermHandler(lockBase());
|
|
532
567
|
|
|
533
568
|
ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto");
|
|
534
569
|
ctx.ui.setFooter(hideFooter);
|
|
@@ -557,17 +592,21 @@ export async function startAuto(
|
|
|
557
592
|
}
|
|
558
593
|
|
|
559
594
|
// Ensure .gitignore has baseline patterns
|
|
560
|
-
|
|
595
|
+
const commitDocs = loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs;
|
|
596
|
+
ensureGitignore(base, { commitDocs });
|
|
561
597
|
untrackRuntimeFiles(base);
|
|
562
598
|
|
|
563
599
|
// Bootstrap .gsd/ if it doesn't exist
|
|
564
600
|
const gsdDir = join(base, ".gsd");
|
|
565
601
|
if (!existsSync(gsdDir)) {
|
|
566
602
|
mkdirSync(join(gsdDir, "milestones"), { recursive: true });
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
603
|
+
// Only commit .gsd/ init when commit_docs is not explicitly false
|
|
604
|
+
if (commitDocs !== false) {
|
|
605
|
+
try {
|
|
606
|
+
nativeAddPaths(base, [".gsd", ".gitignore"]);
|
|
607
|
+
nativeCommit(base, "chore: init gsd");
|
|
608
|
+
} catch { /* nothing to commit */ }
|
|
609
|
+
}
|
|
571
610
|
}
|
|
572
611
|
|
|
573
612
|
// Initialize GitServiceImpl — basePath is set and git repo confirmed
|
|
@@ -658,7 +697,7 @@ export async function startAuto(
|
|
|
658
697
|
// of the repo's default (main/master). Idempotent when the branch is the
|
|
659
698
|
// same; updates the record when started from a different branch (#300).
|
|
660
699
|
if (currentMilestoneId) {
|
|
661
|
-
captureIntegrationBranch(base, currentMilestoneId);
|
|
700
|
+
captureIntegrationBranch(base, currentMilestoneId, { commitDocs });
|
|
662
701
|
setActiveMilestoneId(base, currentMilestoneId);
|
|
663
702
|
}
|
|
664
703
|
|
|
@@ -695,8 +734,8 @@ export async function startAuto(
|
|
|
695
734
|
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
696
735
|
ctx.ui.notify(`Created auto-worktree at ${wtPath}`, "info");
|
|
697
736
|
}
|
|
698
|
-
// Re-register SIGTERM handler with the
|
|
699
|
-
registerSigtermHandler(
|
|
737
|
+
// Re-register SIGTERM handler with the original basePath (lock lives there)
|
|
738
|
+
registerSigtermHandler(originalBasePath);
|
|
700
739
|
} catch (err) {
|
|
701
740
|
// Worktree creation is non-fatal — continue in the project root.
|
|
702
741
|
ctx.ui.notify(
|
|
@@ -952,7 +991,7 @@ export async function handleAgentEnd(
|
|
|
952
991
|
return;
|
|
953
992
|
}
|
|
954
993
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
955
|
-
writeLock(
|
|
994
|
+
writeLock(lockBase(), hookUnit.unitType, hookUnit.unitId, completedUnits.length, sessionFile);
|
|
956
995
|
// Persist hook state so cycle counts survive crashes
|
|
957
996
|
persistHookState(basePath);
|
|
958
997
|
|
|
@@ -1211,7 +1250,7 @@ async function dispatchNextUnit(
|
|
|
1211
1250
|
unitRecoveryCount.clear();
|
|
1212
1251
|
unitLifetimeDispatches.clear();
|
|
1213
1252
|
// Capture integration branch for the new milestone and update git service
|
|
1214
|
-
captureIntegrationBranch(originalBasePath || basePath, mid);
|
|
1253
|
+
captureIntegrationBranch(originalBasePath || basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
|
|
1215
1254
|
}
|
|
1216
1255
|
if (mid) {
|
|
1217
1256
|
currentMilestoneId = mid;
|
|
@@ -1758,7 +1797,7 @@ async function dispatchNextUnit(
|
|
|
1758
1797
|
// Pi appends entries incrementally via appendFileSync, so on crash the
|
|
1759
1798
|
// session file survives with every tool call up to the crash point.
|
|
1760
1799
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1761
|
-
writeLock(
|
|
1800
|
+
writeLock(lockBase(), unitType, unitId, completedUnits.length, sessionFile);
|
|
1762
1801
|
|
|
1763
1802
|
// On crash recovery, prepend the full recovery briefing
|
|
1764
1803
|
// On retry (stuck detection), prepend deep diagnostic from last attempt
|
|
@@ -12,7 +12,7 @@ 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
16
|
import {
|
|
17
17
|
getGlobalGSDPreferencesPath,
|
|
18
18
|
getLegacyGlobalGSDPreferencesPath,
|
|
@@ -178,7 +178,15 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
178
178
|
|
|
179
179
|
if (trimmed === "stop") {
|
|
180
180
|
if (!isAutoActive() && !isAutoPaused()) {
|
|
181
|
-
|
|
181
|
+
// Not running in this process — check for a remote auto-mode session
|
|
182
|
+
const result = stopAutoRemote(process.cwd());
|
|
183
|
+
if (result.found) {
|
|
184
|
+
ctx.ui.notify(`Sent stop signal to auto-mode session (PID ${result.pid}). It will shut down gracefully.`, "info");
|
|
185
|
+
} else if (result.error) {
|
|
186
|
+
ctx.ui.notify(`Failed to stop remote auto-mode: ${result.error}`, "error");
|
|
187
|
+
} else {
|
|
188
|
+
ctx.ui.notify("Auto-mode is not running.", "info");
|
|
189
|
+
}
|
|
182
190
|
return;
|
|
183
191
|
}
|
|
184
192
|
await stopAuto(ctx, pi);
|
|
@@ -502,6 +510,16 @@ async function handlePrefsWizard(
|
|
|
502
510
|
delete git.main_branch;
|
|
503
511
|
}
|
|
504
512
|
}
|
|
513
|
+
// ─── Git commit_docs ────────────────────────────────────────────────────
|
|
514
|
+
const currentCommitDocs = git.commit_docs;
|
|
515
|
+
const commitDocsChoice = await ctx.ui.select(
|
|
516
|
+
`Track .gsd/ planning docs in git${currentCommitDocs !== undefined ? ` (current: ${currentCommitDocs})` : ""}:`,
|
|
517
|
+
["true", "false", "(keep current)"],
|
|
518
|
+
);
|
|
519
|
+
if (commitDocsChoice && commitDocsChoice !== "(keep current)") {
|
|
520
|
+
git.commit_docs = commitDocsChoice === "true";
|
|
521
|
+
}
|
|
522
|
+
|
|
505
523
|
if (Object.keys(git).length > 0) {
|
|
506
524
|
prefs.git = git;
|
|
507
525
|
}
|
|
@@ -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
|
+
}
|
|
@@ -108,6 +108,7 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
|
|
|
108
108
|
- `pre_merge_check`: boolean or `"auto"` — run pre-merge checks before merging a worktree back to the integration branch. `true` always runs, `false` never runs, `"auto"` runs when CI is detected. Default: `false`.
|
|
109
109
|
- `commit_type`: string — override the conventional commit type prefix. Must be one of: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `perf`, `ci`, `build`, `style`. Default: inferred from diff content.
|
|
110
110
|
- `main_branch`: string — the primary branch name for new git repos (e.g., `"main"`, `"master"`, `"trunk"`). Also used by `getMainBranch()` as the preferred branch when auto-detection is ambiguous. Default: `"main"`.
|
|
111
|
+
- `commit_docs`: boolean — when `false`, prevents GSD from committing `.gsd/` planning artifacts to git. The `.gsd/` folder is added to `.gitignore` and kept local-only. Useful for teams where only some members use GSD, or when company policy requires a clean repository. Default: `true`.
|
|
111
112
|
|
|
112
113
|
- `unique_milestone_ids`: boolean — when `true`, generates milestone IDs in `M{seq}-{rand6}` format (e.g. `M001-eh88as`) instead of plain sequential `M001`. Prevents ID collisions in team workflows where multiple contributors create milestones concurrently. Both formats coexist — existing `M001`-style milestones remain valid. Default: `false`.
|
|
113
114
|
|
|
@@ -26,12 +26,16 @@ import { nativeParseRoadmap, nativeExtractSection, nativeParsePlanFile, nativePa
|
|
|
26
26
|
|
|
27
27
|
const CACHE_MAX = 50;
|
|
28
28
|
|
|
29
|
-
/** Fast composite key: length + first/last 100 chars.
|
|
29
|
+
/** Fast composite key: length + first/mid/last 100 chars. The middle sample
|
|
30
|
+
* prevents collisions when only a few characters change in the interior of
|
|
31
|
+
* a file (e.g., a checkbox [ ] → [x] that doesn't alter length or endpoints). */
|
|
30
32
|
function cacheKey(content: string): string {
|
|
31
33
|
const len = content.length;
|
|
32
34
|
const head = content.slice(0, 100);
|
|
35
|
+
const midStart = Math.max(0, Math.floor(len / 2) - 50);
|
|
36
|
+
const mid = len > 200 ? content.slice(midStart, midStart + 100) : '';
|
|
33
37
|
const tail = len > 100 ? content.slice(-100) : '';
|
|
34
|
-
return `${len}:${head}:${tail}`;
|
|
38
|
+
return `${len}:${head}:${mid}:${tail}`;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
const _parseCache = new Map<string, unknown>();
|
|
@@ -47,6 +47,11 @@ export interface GitPreferences {
|
|
|
47
47
|
* - "branch": works directly in the project root (for submodule-heavy repos)
|
|
48
48
|
*/
|
|
49
49
|
isolation?: "worktree" | "branch";
|
|
50
|
+
/** When false, prevents GSD from committing .gsd/ planning artifacts to git.
|
|
51
|
+
* The .gsd/ folder is added to .gitignore and kept local-only.
|
|
52
|
+
* Default: true (planning docs are tracked in git).
|
|
53
|
+
*/
|
|
54
|
+
commit_docs?: boolean;
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
export const VALID_BRANCH_NAME = /^[a-zA-Z0-9_\-\/.]+$/;
|
|
@@ -152,7 +157,7 @@ export function readIntegrationBranch(basePath: string, milestoneId: string): st
|
|
|
152
157
|
*
|
|
153
158
|
* The file is committed immediately so the metadata is persisted in git.
|
|
154
159
|
*/
|
|
155
|
-
export function writeIntegrationBranch(basePath: string, milestoneId: string, branch: string): void {
|
|
160
|
+
export function writeIntegrationBranch(basePath: string, milestoneId: string, branch: string, options?: { commitDocs?: boolean }): void {
|
|
156
161
|
// Don't record slice branches as the integration target
|
|
157
162
|
if (SLICE_BRANCH_RE.test(branch)) return;
|
|
158
163
|
// Validate
|
|
@@ -178,12 +183,15 @@ export function writeIntegrationBranch(basePath: string, milestoneId: string, br
|
|
|
178
183
|
writeFileSync(metaFile, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
179
184
|
|
|
180
185
|
// Commit immediately so the metadata is persisted in git.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
// Skip when commit_docs is explicitly false — .gsd/ is local-only.
|
|
187
|
+
if (options?.commitDocs !== false) {
|
|
188
|
+
try {
|
|
189
|
+
nativeAddPaths(basePath, [metaFile]);
|
|
190
|
+
nativeCommit(basePath, `chore(${milestoneId}): record integration branch`, { allowEmpty: false });
|
|
191
|
+
} catch {
|
|
192
|
+
// Non-fatal — file is on disk even if commit fails (e.g. nothing to commit
|
|
193
|
+
// because the file was already tracked with identical content)
|
|
194
|
+
}
|
|
187
195
|
}
|
|
188
196
|
}
|
|
189
197
|
|
|
@@ -284,7 +292,10 @@ export class GitServiceImpl {
|
|
|
284
292
|
* @param extraExclusions Additional pathspec exclusions beyond RUNTIME_EXCLUSION_PATHS.
|
|
285
293
|
*/
|
|
286
294
|
private smartStage(extraExclusions: readonly string[] = []): void {
|
|
287
|
-
|
|
295
|
+
// When commit_docs is false, exclude the entire .gsd/ directory from staging
|
|
296
|
+
const commitDocsDisabled = this.prefs.commit_docs === false;
|
|
297
|
+
const gsdExclusion = commitDocsDisabled ? [".gsd/"] : [];
|
|
298
|
+
const allExclusions = [...RUNTIME_EXCLUSION_PATHS, ...gsdExclusion, ...extraExclusions];
|
|
288
299
|
|
|
289
300
|
// One-time cleanup: if runtime files are already tracked in the index
|
|
290
301
|
// (from older versions where the fallback bug staged them), untrack them
|
|
@@ -78,15 +78,26 @@ const BASELINE_PATTERNS = [
|
|
|
78
78
|
* Ensure basePath/.gitignore contains all baseline patterns.
|
|
79
79
|
* Creates the file if missing; appends only missing lines if it exists.
|
|
80
80
|
* Returns true if the file was created or modified, false if already complete.
|
|
81
|
+
*
|
|
82
|
+
* When `commitDocs` is false, the entire `.gsd/` directory is added to
|
|
83
|
+
* .gitignore instead of individual runtime patterns, keeping all GSD
|
|
84
|
+
* artifacts local-only.
|
|
81
85
|
*/
|
|
82
|
-
export function ensureGitignore(basePath: string): boolean {
|
|
86
|
+
export function ensureGitignore(basePath: string, options?: { commitDocs?: boolean }): boolean {
|
|
83
87
|
const gitignorePath = join(basePath, ".gitignore");
|
|
88
|
+
const commitDocs = options?.commitDocs !== false; // default true
|
|
84
89
|
|
|
85
90
|
let existing = "";
|
|
86
91
|
if (existsSync(gitignorePath)) {
|
|
87
92
|
existing = readFileSync(gitignorePath, "utf-8");
|
|
88
93
|
}
|
|
89
94
|
|
|
95
|
+
// When commit_docs is false, ensure blanket ".gsd/" is in .gitignore
|
|
96
|
+
// and skip the self-heal that would remove it.
|
|
97
|
+
if (!commitDocs) {
|
|
98
|
+
return ensureBlanketGsdIgnore(gitignorePath, existing);
|
|
99
|
+
}
|
|
100
|
+
|
|
90
101
|
// Self-heal: remove blanket ".gsd/" lines from pre-v2.14.0 projects.
|
|
91
102
|
// The blanket ignore prevented planning artifacts (.gsd/milestones/) from
|
|
92
103
|
// being tracked in git, causing artifacts to vanish in worktrees and
|
|
@@ -203,7 +214,7 @@ See \`~/.gsd/agent/extensions/gsd/docs/preferences-reference.md\` for full field
|
|
|
203
214
|
- \`models\`: Model preferences for specific task types
|
|
204
215
|
- \`skill_discovery\`: Automatic skill detection preferences
|
|
205
216
|
- \`auto_supervisor\`: Supervision and gating rules for autonomous modes
|
|
206
|
-
- \`git\`: Git preferences — \`main_branch\` (default branch name for new repos, e.g., "main", "master", "trunk"), \`auto_push\`, \`snapshots\`, etc.
|
|
217
|
+
- \`git\`: Git preferences — \`main_branch\` (default branch name for new repos, e.g., "main", "master", "trunk"), \`auto_push\`, \`snapshots\`, \`commit_docs\` (set to \`false\` to keep .gsd/ local-only), etc.
|
|
207
218
|
|
|
208
219
|
## Examples
|
|
209
220
|
|
|
@@ -224,3 +235,31 @@ custom_instructions:
|
|
|
224
235
|
return true;
|
|
225
236
|
}
|
|
226
237
|
|
|
238
|
+
/**
|
|
239
|
+
* When commit_docs is false, ensure `.gsd/` is in .gitignore as a blanket
|
|
240
|
+
* pattern. This keeps all GSD artifacts local-only.
|
|
241
|
+
* Returns true if the file was modified, false if already complete.
|
|
242
|
+
*/
|
|
243
|
+
function ensureBlanketGsdIgnore(gitignorePath: string, existing: string): boolean {
|
|
244
|
+
const existingLines = new Set(
|
|
245
|
+
existing
|
|
246
|
+
.split("\n")
|
|
247
|
+
.map((l) => l.trim())
|
|
248
|
+
.filter((l) => l && !l.startsWith("#")),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// Already has blanket .gsd/ ignore
|
|
252
|
+
if (existingLines.has(".gsd/") || existingLines.has(".gsd")) return false;
|
|
253
|
+
|
|
254
|
+
const block = [
|
|
255
|
+
"",
|
|
256
|
+
"# ── GSD (local-only, commit_docs: false) ──",
|
|
257
|
+
".gsd/",
|
|
258
|
+
"",
|
|
259
|
+
].join("\n");
|
|
260
|
+
|
|
261
|
+
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
262
|
+
writeFileSync(gitignorePath, existing + prefix + block, "utf-8");
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
@@ -712,7 +712,8 @@ export async function showSmartEntry(
|
|
|
712
712
|
}
|
|
713
713
|
|
|
714
714
|
// ── Ensure .gitignore has baseline patterns ──────────────────────────
|
|
715
|
-
|
|
715
|
+
const commitDocs = loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs;
|
|
716
|
+
ensureGitignore(basePath, { commitDocs });
|
|
716
717
|
untrackRuntimeFiles(basePath);
|
|
717
718
|
|
|
718
719
|
// ── No GSD project OR no milestone → Create first/next milestone ────
|
|
@@ -723,11 +724,14 @@ export async function showSmartEntry(
|
|
|
723
724
|
|
|
724
725
|
// ── Create PREFERENCES.md template ────────────────────────────────
|
|
725
726
|
ensurePreferences(basePath);
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
727
|
+
// Only commit .gsd/ init when commit_docs is not explicitly false
|
|
728
|
+
if (commitDocs !== false) {
|
|
729
|
+
try {
|
|
730
|
+
nativeAddPaths(basePath, [".gsd", ".gitignore"]);
|
|
731
|
+
nativeCommit(basePath, "chore: init gsd");
|
|
732
|
+
} catch {
|
|
733
|
+
// nothing to commit — that's fine
|
|
734
|
+
}
|
|
731
735
|
}
|
|
732
736
|
}
|
|
733
737
|
|
|
@@ -303,6 +303,50 @@ export function formatCost(cost: number): string {
|
|
|
303
303
|
return `$${n.toFixed(2)}`;
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
// ─── Budget Prediction ────────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Calculate average cost per unit type from completed units.
|
|
310
|
+
* Returns a Map from unit type to average cost in USD.
|
|
311
|
+
*/
|
|
312
|
+
export function getAverageCostPerUnitType(units: UnitMetrics[]): Map<string, number> {
|
|
313
|
+
const sums = new Map<string, { total: number; count: number }>();
|
|
314
|
+
for (const u of units) {
|
|
315
|
+
const entry = sums.get(u.type) ?? { total: 0, count: 0 };
|
|
316
|
+
entry.total += u.cost;
|
|
317
|
+
entry.count += 1;
|
|
318
|
+
sums.set(u.type, entry);
|
|
319
|
+
}
|
|
320
|
+
const avgs = new Map<string, number>();
|
|
321
|
+
for (const [type, { total, count }] of sums) {
|
|
322
|
+
avgs.set(type, total / count);
|
|
323
|
+
}
|
|
324
|
+
return avgs;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Estimate remaining cost given average costs and remaining unit counts.
|
|
329
|
+
* @param avgCosts - Average cost per unit type
|
|
330
|
+
* @param remainingUnits - Array of unit types still to dispatch
|
|
331
|
+
* @param fallbackAvg - Fallback average if unit type not seen before
|
|
332
|
+
* @returns Estimated remaining cost in USD
|
|
333
|
+
*/
|
|
334
|
+
export function predictRemainingCost(
|
|
335
|
+
avgCosts: Map<string, number>,
|
|
336
|
+
remainingUnits: string[],
|
|
337
|
+
fallbackAvg?: number,
|
|
338
|
+
): number {
|
|
339
|
+
// If no averages available, use overall average as fallback
|
|
340
|
+
const allAvgs = [...avgCosts.values()];
|
|
341
|
+
const overallAvg = fallbackAvg ?? (allAvgs.length > 0 ? allAvgs.reduce((a, b) => a + b, 0) / allAvgs.length : 0);
|
|
342
|
+
|
|
343
|
+
let total = 0;
|
|
344
|
+
for (const unitType of remainingUnits) {
|
|
345
|
+
total += avgCosts.get(unitType) ?? overallAvg;
|
|
346
|
+
}
|
|
347
|
+
return total;
|
|
348
|
+
}
|
|
349
|
+
|
|
306
350
|
/**
|
|
307
351
|
* Compute a projected remaining cost based on completed slice averages.
|
|
308
352
|
*
|
|
@@ -17,6 +17,10 @@ const GIT_NO_PROMPT_ENV = {
|
|
|
17
17
|
GIT_SVN_ID: "",
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
// Issue #453: keep auto-mode bookkeeping on the stable git CLI path unless a
|
|
21
|
+
// caller explicitly opts into the native helper.
|
|
22
|
+
const NATIVE_GSD_GIT_ENABLED = process.env.GSD_ENABLE_NATIVE_GSD_GIT === "1";
|
|
23
|
+
|
|
20
24
|
// ─── Native Module Types ──────────────────────────────────────────────────
|
|
21
25
|
|
|
22
26
|
interface GitDiffStat {
|
|
@@ -116,6 +120,7 @@ let loadAttempted = false;
|
|
|
116
120
|
function loadNative(): typeof nativeModule {
|
|
117
121
|
if (loadAttempted) return nativeModule;
|
|
118
122
|
loadAttempted = true;
|
|
123
|
+
if (!NATIVE_GSD_GIT_ENABLED) return nativeModule;
|
|
119
124
|
|
|
120
125
|
try {
|
|
121
126
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|