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
|
@@ -3,7 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { isAbsolute, join } from "node:path";
|
|
4
4
|
import { getAgentDir } from "@gsd/pi-coding-agent";
|
|
5
5
|
import type { GitPreferences } from "./git-service.js";
|
|
6
|
-
import type { PostUnitHookConfig, PreDispatchHookConfig, BudgetEnforcementMode, NotificationPreferences } from "./types.js";
|
|
6
|
+
import type { PostUnitHookConfig, PreDispatchHookConfig, BudgetEnforcementMode, NotificationPreferences, TokenProfile, InlineLevel, PhaseSkipPreferences } from "./types.js";
|
|
7
7
|
import { VALID_BRANCH_NAME } from "./git-service.js";
|
|
8
8
|
|
|
9
9
|
const GLOBAL_PREFERENCES_PATH = join(homedir(), ".gsd", "preferences.md");
|
|
@@ -36,6 +36,8 @@ const KNOWN_PREFERENCE_KEYS = new Set<string>([
|
|
|
36
36
|
"git",
|
|
37
37
|
"post_unit_hooks",
|
|
38
38
|
"pre_dispatch_hooks",
|
|
39
|
+
"token_profile",
|
|
40
|
+
"phases",
|
|
39
41
|
]);
|
|
40
42
|
|
|
41
43
|
export interface GSDSkillRule {
|
|
@@ -66,7 +68,9 @@ export interface GSDModelConfig {
|
|
|
66
68
|
research?: string;
|
|
67
69
|
planning?: string;
|
|
68
70
|
execution?: string;
|
|
71
|
+
execution_simple?: string;
|
|
69
72
|
completion?: string;
|
|
73
|
+
subagent?: string;
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
/**
|
|
@@ -77,7 +81,9 @@ export interface GSDModelConfigV2 {
|
|
|
77
81
|
research?: string | GSDPhaseModelConfig;
|
|
78
82
|
planning?: string | GSDPhaseModelConfig;
|
|
79
83
|
execution?: string | GSDPhaseModelConfig;
|
|
84
|
+
execution_simple?: string | GSDPhaseModelConfig;
|
|
80
85
|
completion?: string | GSDPhaseModelConfig;
|
|
86
|
+
subagent?: string | GSDPhaseModelConfig;
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
/** Normalized model selection with resolved fallbacks */
|
|
@@ -122,6 +128,8 @@ export interface GSDPreferences {
|
|
|
122
128
|
git?: GitPreferences;
|
|
123
129
|
post_unit_hooks?: PostUnitHookConfig[];
|
|
124
130
|
pre_dispatch_hooks?: PreDispatchHookConfig[];
|
|
131
|
+
token_profile?: TokenProfile;
|
|
132
|
+
phases?: PhaseSkipPreferences;
|
|
125
133
|
}
|
|
126
134
|
|
|
127
135
|
export interface LoadedGSDPreferences {
|
|
@@ -631,11 +639,19 @@ export function resolveModelWithFallbacksForUnit(unitType: string): ResolvedMode
|
|
|
631
639
|
case "execute-task":
|
|
632
640
|
phaseConfig = m.execution;
|
|
633
641
|
break;
|
|
642
|
+
case "execute-task-simple":
|
|
643
|
+
phaseConfig = m.execution_simple ?? m.execution;
|
|
644
|
+
break;
|
|
634
645
|
case "complete-slice":
|
|
635
646
|
case "run-uat":
|
|
636
647
|
phaseConfig = m.completion;
|
|
637
648
|
break;
|
|
638
649
|
default:
|
|
650
|
+
// Subagent unit types (e.g., "subagent", "subagent/scout")
|
|
651
|
+
if (unitType === "subagent" || unitType.startsWith("subagent/")) {
|
|
652
|
+
phaseConfig = m.subagent;
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
639
655
|
return undefined;
|
|
640
656
|
}
|
|
641
657
|
|
|
@@ -670,6 +686,73 @@ export function resolveAutoSupervisorConfig(): AutoSupervisorConfig {
|
|
|
670
686
|
};
|
|
671
687
|
}
|
|
672
688
|
|
|
689
|
+
// ─── Token Profile Resolution ─────────────────────────────────────────────
|
|
690
|
+
|
|
691
|
+
const VALID_TOKEN_PROFILES = new Set<TokenProfile>(["budget", "balanced", "quality"]);
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Resolve profile defaults for a given token profile tier.
|
|
695
|
+
* Returns a partial GSDPreferences that is used as the base layer —
|
|
696
|
+
* explicit user preferences always override these defaults.
|
|
697
|
+
*/
|
|
698
|
+
export function resolveProfileDefaults(profile: TokenProfile): Partial<GSDPreferences> {
|
|
699
|
+
switch (profile) {
|
|
700
|
+
case "budget":
|
|
701
|
+
return {
|
|
702
|
+
models: {
|
|
703
|
+
planning: "claude-sonnet-4-5-20250514",
|
|
704
|
+
execution: "claude-sonnet-4-5-20250514",
|
|
705
|
+
execution_simple: "claude-haiku-4-5-20250414",
|
|
706
|
+
completion: "claude-haiku-4-5-20250414",
|
|
707
|
+
subagent: "claude-haiku-4-5-20250414",
|
|
708
|
+
},
|
|
709
|
+
phases: {
|
|
710
|
+
skip_research: true,
|
|
711
|
+
skip_reassess: true,
|
|
712
|
+
skip_slice_research: true,
|
|
713
|
+
},
|
|
714
|
+
};
|
|
715
|
+
case "balanced":
|
|
716
|
+
return {
|
|
717
|
+
models: {
|
|
718
|
+
subagent: "claude-sonnet-4-5-20250514",
|
|
719
|
+
},
|
|
720
|
+
phases: {
|
|
721
|
+
skip_slice_research: true,
|
|
722
|
+
},
|
|
723
|
+
};
|
|
724
|
+
case "quality":
|
|
725
|
+
return {
|
|
726
|
+
models: {},
|
|
727
|
+
phases: {},
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Resolve the effective token profile from preferences.
|
|
734
|
+
* Returns "balanced" when no profile is set (D046).
|
|
735
|
+
*/
|
|
736
|
+
export function resolveEffectiveProfile(): TokenProfile {
|
|
737
|
+
const prefs = loadEffectiveGSDPreferences();
|
|
738
|
+
const profile = prefs?.preferences.token_profile;
|
|
739
|
+
if (profile && VALID_TOKEN_PROFILES.has(profile)) return profile;
|
|
740
|
+
return "balanced";
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Resolve the inline level from the active token profile.
|
|
745
|
+
* budget → minimal, balanced → standard, quality → full.
|
|
746
|
+
*/
|
|
747
|
+
export function resolveInlineLevel(): InlineLevel {
|
|
748
|
+
const profile = resolveEffectiveProfile();
|
|
749
|
+
switch (profile) {
|
|
750
|
+
case "budget": return "minimal";
|
|
751
|
+
case "balanced": return "standard";
|
|
752
|
+
case "quality": return "full";
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
673
756
|
function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPreferences {
|
|
674
757
|
return {
|
|
675
758
|
version: override.version ?? base.version,
|
|
@@ -697,6 +780,10 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
|
|
|
697
780
|
: undefined,
|
|
698
781
|
post_unit_hooks: mergePostUnitHooks(base.post_unit_hooks, override.post_unit_hooks),
|
|
699
782
|
pre_dispatch_hooks: mergePreDispatchHooks(base.pre_dispatch_hooks, override.pre_dispatch_hooks),
|
|
783
|
+
token_profile: override.token_profile ?? base.token_profile,
|
|
784
|
+
phases: (base.phases || override.phases)
|
|
785
|
+
? { ...(base.phases ?? {}), ...(override.phases ?? {}) }
|
|
786
|
+
: undefined,
|
|
700
787
|
};
|
|
701
788
|
}
|
|
702
789
|
|
|
@@ -803,6 +890,36 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
803
890
|
}
|
|
804
891
|
}
|
|
805
892
|
|
|
893
|
+
// ─── Token Profile ─────────────────────────────────────────────────
|
|
894
|
+
if (preferences.token_profile !== undefined) {
|
|
895
|
+
if (typeof preferences.token_profile === "string" && VALID_TOKEN_PROFILES.has(preferences.token_profile as TokenProfile)) {
|
|
896
|
+
validated.token_profile = preferences.token_profile as TokenProfile;
|
|
897
|
+
} else {
|
|
898
|
+
errors.push(`token_profile must be one of: budget, balanced, quality`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// ─── Phase Skip Preferences ─────────────────────────────────────────
|
|
903
|
+
if (preferences.phases !== undefined) {
|
|
904
|
+
if (typeof preferences.phases === "object" && preferences.phases !== null) {
|
|
905
|
+
const validatedPhases: PhaseSkipPreferences = {};
|
|
906
|
+
const p = preferences.phases as Record<string, unknown>;
|
|
907
|
+
if (p.skip_research !== undefined) validatedPhases.skip_research = !!p.skip_research;
|
|
908
|
+
if (p.skip_reassess !== undefined) validatedPhases.skip_reassess = !!p.skip_reassess;
|
|
909
|
+
if (p.skip_slice_research !== undefined) validatedPhases.skip_slice_research = !!p.skip_slice_research;
|
|
910
|
+
// Warn on unknown phase keys
|
|
911
|
+
const knownPhaseKeys = new Set(["skip_research", "skip_reassess", "skip_slice_research"]);
|
|
912
|
+
for (const key of Object.keys(p)) {
|
|
913
|
+
if (!knownPhaseKeys.has(key)) {
|
|
914
|
+
warnings.push(`unknown phases key "${key}" — ignored`);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
validated.phases = validatedPhases;
|
|
918
|
+
} else {
|
|
919
|
+
errors.push(`phases must be an object`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
806
923
|
// ─── Context Pause Threshold ────────────────────────────────────────
|
|
807
924
|
if (preferences.context_pause_threshold !== undefined) {
|
|
808
925
|
const raw = preferences.context_pause_threshold;
|
|
@@ -1046,6 +1163,10 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
1046
1163
|
errors.push("git.isolation must be one of: worktree, branch");
|
|
1047
1164
|
}
|
|
1048
1165
|
}
|
|
1166
|
+
if (g.commit_docs !== undefined) {
|
|
1167
|
+
if (typeof g.commit_docs === "boolean") git.commit_docs = g.commit_docs;
|
|
1168
|
+
else errors.push("git.commit_docs must be a boolean");
|
|
1169
|
+
}
|
|
1049
1170
|
// Deprecated: merge_to_main is ignored (branchless architecture).
|
|
1050
1171
|
if (g.merge_to_main !== undefined) {
|
|
1051
1172
|
warnings.push("git.merge_to_main is deprecated — milestone-level merge is now always used. Remove this setting.");
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// GSD Extension — Routing History (Adaptive Learning)
|
|
2
|
+
// Tracks success/failure per tier per unit-type pattern to improve
|
|
3
|
+
// classification accuracy over time.
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { gsdRoot } from "./paths.js";
|
|
8
|
+
import type { ComplexityTier } from "./types.js";
|
|
9
|
+
|
|
10
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export interface TierOutcome {
|
|
13
|
+
success: number;
|
|
14
|
+
fail: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PatternHistory {
|
|
18
|
+
light: TierOutcome;
|
|
19
|
+
standard: TierOutcome;
|
|
20
|
+
heavy: TierOutcome;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RoutingHistoryData {
|
|
24
|
+
version: 1;
|
|
25
|
+
/** Keyed by pattern string, e.g. "execute-task:docs" or "complete-slice" */
|
|
26
|
+
patterns: Record<string, PatternHistory>;
|
|
27
|
+
/** User feedback entries (from /gsd:rate-unit) */
|
|
28
|
+
feedback: FeedbackEntry[];
|
|
29
|
+
/** Last updated timestamp */
|
|
30
|
+
updatedAt: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FeedbackEntry {
|
|
34
|
+
unitType: string;
|
|
35
|
+
unitId: string;
|
|
36
|
+
tier: ComplexityTier;
|
|
37
|
+
rating: "over" | "under" | "ok";
|
|
38
|
+
timestamp: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
const HISTORY_FILE = "routing-history.json";
|
|
44
|
+
const ROLLING_WINDOW = 50; // only consider last N entries per pattern
|
|
45
|
+
const FAILURE_THRESHOLD = 0.20; // >20% failure rate triggers tier bump
|
|
46
|
+
const FEEDBACK_WEIGHT = 2; // feedback signals count 2x vs automatic
|
|
47
|
+
|
|
48
|
+
// ─── In-Memory State ─────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
let history: RoutingHistoryData | null = null;
|
|
51
|
+
let historyBasePath = "";
|
|
52
|
+
|
|
53
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Initialize routing history for a project.
|
|
57
|
+
*/
|
|
58
|
+
export function initRoutingHistory(base: string): void {
|
|
59
|
+
historyBasePath = base;
|
|
60
|
+
history = loadHistory(base);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Reset routing history state.
|
|
65
|
+
*/
|
|
66
|
+
export function resetRoutingHistory(): void {
|
|
67
|
+
history = null;
|
|
68
|
+
historyBasePath = "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Record the outcome of a unit dispatch.
|
|
73
|
+
*
|
|
74
|
+
* @param unitType The unit type (e.g. "execute-task")
|
|
75
|
+
* @param tier The tier that was used
|
|
76
|
+
* @param success Whether the unit completed successfully
|
|
77
|
+
* @param tags Optional tags from task metadata (e.g. ["docs", "test"])
|
|
78
|
+
*/
|
|
79
|
+
export function recordOutcome(
|
|
80
|
+
unitType: string,
|
|
81
|
+
tier: ComplexityTier,
|
|
82
|
+
success: boolean,
|
|
83
|
+
tags?: string[],
|
|
84
|
+
): void {
|
|
85
|
+
if (!history) return;
|
|
86
|
+
|
|
87
|
+
// Record for the base unit type
|
|
88
|
+
const basePattern = unitType;
|
|
89
|
+
ensurePattern(basePattern);
|
|
90
|
+
const outcome = history.patterns[basePattern][tier];
|
|
91
|
+
if (success) outcome.success++;
|
|
92
|
+
else outcome.fail++;
|
|
93
|
+
|
|
94
|
+
// Record for tag-specific patterns (e.g. "execute-task:docs")
|
|
95
|
+
if (tags && tags.length > 0) {
|
|
96
|
+
for (const tag of tags) {
|
|
97
|
+
const tagPattern = `${unitType}:${tag}`;
|
|
98
|
+
ensurePattern(tagPattern);
|
|
99
|
+
const tagOutcome = history.patterns[tagPattern][tier];
|
|
100
|
+
if (success) tagOutcome.success++;
|
|
101
|
+
else tagOutcome.fail++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Apply rolling window — cap total entries per tier per pattern
|
|
106
|
+
for (const pattern of Object.keys(history.patterns)) {
|
|
107
|
+
const p = history.patterns[pattern];
|
|
108
|
+
for (const t of ["light", "standard", "heavy"] as const) {
|
|
109
|
+
const total = p[t].success + p[t].fail;
|
|
110
|
+
if (total > ROLLING_WINDOW) {
|
|
111
|
+
const scale = ROLLING_WINDOW / total;
|
|
112
|
+
p[t].success = Math.round(p[t].success * scale);
|
|
113
|
+
p[t].fail = Math.round(p[t].fail * scale);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
history.updatedAt = new Date().toISOString();
|
|
119
|
+
saveHistory(historyBasePath, history);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Record user feedback for the last completed unit.
|
|
124
|
+
*/
|
|
125
|
+
export function recordFeedback(
|
|
126
|
+
unitType: string,
|
|
127
|
+
unitId: string,
|
|
128
|
+
tier: ComplexityTier,
|
|
129
|
+
rating: "over" | "under" | "ok",
|
|
130
|
+
): void {
|
|
131
|
+
if (!history) return;
|
|
132
|
+
|
|
133
|
+
history.feedback.push({
|
|
134
|
+
unitType,
|
|
135
|
+
unitId,
|
|
136
|
+
tier,
|
|
137
|
+
rating,
|
|
138
|
+
timestamp: new Date().toISOString(),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Cap feedback array at 200 entries
|
|
142
|
+
if (history.feedback.length > 200) {
|
|
143
|
+
history.feedback = history.feedback.slice(-200);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Apply feedback as weighted outcome
|
|
147
|
+
const pattern = unitType;
|
|
148
|
+
ensurePattern(pattern);
|
|
149
|
+
|
|
150
|
+
if (rating === "over") {
|
|
151
|
+
// User says this could have used a simpler model → record as success at current tier
|
|
152
|
+
// and also as success at one tier lower (encourages more downgrading)
|
|
153
|
+
const lower = tierBelow(tier);
|
|
154
|
+
if (lower) {
|
|
155
|
+
const outcomes = history.patterns[pattern][lower];
|
|
156
|
+
outcomes.success += FEEDBACK_WEIGHT;
|
|
157
|
+
}
|
|
158
|
+
} else if (rating === "under") {
|
|
159
|
+
// User says this needed a better model → record as failure at current tier
|
|
160
|
+
const outcomes = history.patterns[pattern][tier];
|
|
161
|
+
outcomes.fail += FEEDBACK_WEIGHT;
|
|
162
|
+
}
|
|
163
|
+
// "ok" = no adjustment needed
|
|
164
|
+
|
|
165
|
+
history.updatedAt = new Date().toISOString();
|
|
166
|
+
saveHistory(historyBasePath, history);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get the recommended tier adjustment for a given pattern.
|
|
171
|
+
* Returns the tier to bump to if the failure rate exceeds threshold,
|
|
172
|
+
* or null if no adjustment is needed.
|
|
173
|
+
*/
|
|
174
|
+
export function getAdaptiveTierAdjustment(
|
|
175
|
+
unitType: string,
|
|
176
|
+
currentTier: ComplexityTier,
|
|
177
|
+
tags?: string[],
|
|
178
|
+
): ComplexityTier | null {
|
|
179
|
+
if (!history) return null;
|
|
180
|
+
|
|
181
|
+
// Check tag-specific patterns first (more specific)
|
|
182
|
+
if (tags && tags.length > 0) {
|
|
183
|
+
for (const tag of tags) {
|
|
184
|
+
const tagPattern = `${unitType}:${tag}`;
|
|
185
|
+
const adjustment = checkPatternFailureRate(tagPattern, currentTier);
|
|
186
|
+
if (adjustment) return adjustment;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Fall back to base pattern
|
|
191
|
+
return checkPatternFailureRate(unitType, currentTier);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Clear all routing history (user-triggered reset).
|
|
196
|
+
*/
|
|
197
|
+
export function clearRoutingHistory(base: string): void {
|
|
198
|
+
history = createEmptyHistory();
|
|
199
|
+
saveHistory(base, history);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get current history data (for display/debugging).
|
|
204
|
+
*/
|
|
205
|
+
export function getRoutingHistory(): RoutingHistoryData | null {
|
|
206
|
+
return history;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ─── Internal ────────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
function checkPatternFailureRate(
|
|
212
|
+
pattern: string,
|
|
213
|
+
tier: ComplexityTier,
|
|
214
|
+
): ComplexityTier | null {
|
|
215
|
+
if (!history?.patterns[pattern]) return null;
|
|
216
|
+
|
|
217
|
+
const outcomes = history.patterns[pattern][tier];
|
|
218
|
+
const total = outcomes.success + outcomes.fail;
|
|
219
|
+
if (total < 3) return null; // Not enough data
|
|
220
|
+
|
|
221
|
+
const failureRate = outcomes.fail / total;
|
|
222
|
+
if (failureRate > FAILURE_THRESHOLD) {
|
|
223
|
+
// Bump to next tier
|
|
224
|
+
return tierAbove(tier);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function tierAbove(tier: ComplexityTier): ComplexityTier | null {
|
|
231
|
+
switch (tier) {
|
|
232
|
+
case "light": return "standard";
|
|
233
|
+
case "standard": return "heavy";
|
|
234
|
+
case "heavy": return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function tierBelow(tier: ComplexityTier): ComplexityTier | null {
|
|
239
|
+
switch (tier) {
|
|
240
|
+
case "light": return null;
|
|
241
|
+
case "standard": return "light";
|
|
242
|
+
case "heavy": return "standard";
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function ensurePattern(pattern: string): void {
|
|
247
|
+
if (!history) return;
|
|
248
|
+
if (!history.patterns[pattern]) {
|
|
249
|
+
history.patterns[pattern] = {
|
|
250
|
+
light: { success: 0, fail: 0 },
|
|
251
|
+
standard: { success: 0, fail: 0 },
|
|
252
|
+
heavy: { success: 0, fail: 0 },
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function createEmptyHistory(): RoutingHistoryData {
|
|
258
|
+
return {
|
|
259
|
+
version: 1,
|
|
260
|
+
patterns: {},
|
|
261
|
+
feedback: [],
|
|
262
|
+
updatedAt: new Date().toISOString(),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function historyPath(base: string): string {
|
|
267
|
+
return join(gsdRoot(base), HISTORY_FILE);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function loadHistory(base: string): RoutingHistoryData {
|
|
271
|
+
try {
|
|
272
|
+
const raw = readFileSync(historyPath(base), "utf-8");
|
|
273
|
+
const parsed = JSON.parse(raw);
|
|
274
|
+
if (parsed.version === 1 && parsed.patterns) {
|
|
275
|
+
return parsed as RoutingHistoryData;
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
// File doesn't exist or is corrupt — start fresh
|
|
279
|
+
}
|
|
280
|
+
return createEmptyHistory();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function saveHistory(base: string, data: RoutingHistoryData): void {
|
|
284
|
+
try {
|
|
285
|
+
mkdirSync(gsdRoot(base), { recursive: true });
|
|
286
|
+
writeFileSync(historyPath(base), JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
287
|
+
} catch {
|
|
288
|
+
// Non-fatal — don't let history failures break auto-mode
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -7,6 +7,7 @@ import { randomUUID } from "node:crypto";
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
resolveExpectedArtifactPath,
|
|
10
|
+
verifyExpectedArtifact,
|
|
10
11
|
diagnoseExpectedArtifact,
|
|
11
12
|
buildLoopRemediationSteps,
|
|
12
13
|
completedKeysPath,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
removePersistedKey,
|
|
15
16
|
loadPersistedKeys,
|
|
16
17
|
} from "../auto-recovery.ts";
|
|
18
|
+
import { parseRoadmap, clearParseCache } from "../files.ts";
|
|
17
19
|
|
|
18
20
|
function makeTmpBase(): string {
|
|
19
21
|
const base = join(tmpdir(), `gsd-test-${randomUUID()}`);
|
|
@@ -270,3 +272,51 @@ test("removePersistedKey is safe when file doesn't exist", () => {
|
|
|
270
272
|
cleanup(base);
|
|
271
273
|
}
|
|
272
274
|
});
|
|
275
|
+
|
|
276
|
+
// ─── verifyExpectedArtifact: parse cache collision regression ─────────────
|
|
277
|
+
|
|
278
|
+
test("verifyExpectedArtifact detects roadmap [x] change despite parse cache", () => {
|
|
279
|
+
// Regression test: cacheKey collision when [ ] → [x] doesn't change
|
|
280
|
+
// file length or first/last 100 chars. Without the fix, parseRoadmap
|
|
281
|
+
// returns stale cached data with done=false even though the file has [x].
|
|
282
|
+
const base = makeTmpBase();
|
|
283
|
+
try {
|
|
284
|
+
// Build a roadmap long enough that the [x] change is outside the first/last 100 chars
|
|
285
|
+
const padding = "A".repeat(200);
|
|
286
|
+
const roadmapBefore = [
|
|
287
|
+
`# M001: Test Milestone ${padding}`,
|
|
288
|
+
"",
|
|
289
|
+
"## Slices",
|
|
290
|
+
"",
|
|
291
|
+
"- [ ] **S01: First slice** `risk:low`",
|
|
292
|
+
"",
|
|
293
|
+
`## Footer ${padding}`,
|
|
294
|
+
].join("\n");
|
|
295
|
+
const roadmapAfter = roadmapBefore.replace("- [ ] **S01:", "- [x] **S01:");
|
|
296
|
+
|
|
297
|
+
// Verify lengths are identical (the key collision condition)
|
|
298
|
+
assert.equal(roadmapBefore.length, roadmapAfter.length);
|
|
299
|
+
|
|
300
|
+
// Populate parse cache with the pre-edit roadmap
|
|
301
|
+
const before = parseRoadmap(roadmapBefore);
|
|
302
|
+
const sliceBefore = before.slices.find(s => s.id === "S01");
|
|
303
|
+
assert.ok(sliceBefore);
|
|
304
|
+
assert.equal(sliceBefore!.done, false);
|
|
305
|
+
|
|
306
|
+
// Now write the post-edit roadmap to disk and create required artifacts
|
|
307
|
+
const roadmapPath = join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md");
|
|
308
|
+
writeFileSync(roadmapPath, roadmapAfter);
|
|
309
|
+
const summaryPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-SUMMARY.md");
|
|
310
|
+
writeFileSync(summaryPath, "# Summary\nDone.");
|
|
311
|
+
const uatPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-UAT.md");
|
|
312
|
+
writeFileSync(uatPath, "# UAT\nPassed.");
|
|
313
|
+
|
|
314
|
+
// verifyExpectedArtifact should see the [x] despite the parse cache
|
|
315
|
+
// having the [ ] version. The fix clears the parse cache inside verify.
|
|
316
|
+
const verified = verifyExpectedArtifact("complete-slice", "M001/S01", base);
|
|
317
|
+
assert.equal(verified, true, "verifyExpectedArtifact should return true when roadmap has [x]");
|
|
318
|
+
} finally {
|
|
319
|
+
clearParseCache();
|
|
320
|
+
cleanup(base);
|
|
321
|
+
}
|
|
322
|
+
});
|