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
|
@@ -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
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
import type { Roadmap, BoundaryMapEntry, RoadmapSliceEntry, RiskLevel } from './types.js';
|
|
8
8
|
|
|
9
|
+
// Issue #453: auto-mode post-turn reconciliation must stay on the stable JS path
|
|
10
|
+
// unless the native parser is explicitly requested.
|
|
11
|
+
const NATIVE_GSD_PARSER_ENABLED = process.env.GSD_ENABLE_NATIVE_GSD_PARSER === "1";
|
|
12
|
+
|
|
9
13
|
let nativeModule: {
|
|
10
14
|
parseFrontmatter: (content: string) => { metadata: string; body: string };
|
|
11
15
|
extractSection: (content: string, heading: string, level?: number) => { content: string; found: boolean };
|
|
@@ -29,6 +33,7 @@ let loadAttempted = false;
|
|
|
29
33
|
function loadNative(): typeof nativeModule {
|
|
30
34
|
if (loadAttempted) return nativeModule;
|
|
31
35
|
loadAttempted = true;
|
|
36
|
+
if (!NATIVE_GSD_PARSER_ENABLED) return nativeModule;
|
|
32
37
|
|
|
33
38
|
try {
|
|
34
39
|
// Dynamic import to avoid hard dependency - fails gracefully if native module not built
|