gsd-pi 2.16.0 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/dist/onboarding.js +2 -2
- package/dist/remote-questions-config.d.ts +10 -0
- package/dist/remote-questions-config.js +36 -0
- package/dist/resources/extensions/gsd/activity-log.ts +37 -7
- package/dist/resources/extensions/gsd/auto-dashboard.ts +4 -0
- package/dist/resources/extensions/gsd/auto-dispatch.ts +9 -3
- package/dist/resources/extensions/gsd/auto-prompts.ts +91 -42
- package/dist/resources/extensions/gsd/auto-recovery.ts +7 -2
- package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/dist/resources/extensions/gsd/auto.ts +177 -25
- package/dist/resources/extensions/gsd/commands.ts +264 -23
- package/dist/resources/extensions/gsd/complexity.ts +236 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +202 -2
- package/dist/resources/extensions/gsd/files.ts +129 -3
- package/dist/resources/extensions/gsd/git-service.ts +19 -8
- package/dist/resources/extensions/gsd/gitignore.ts +41 -2
- package/dist/resources/extensions/gsd/guided-flow.ts +247 -10
- package/dist/resources/extensions/gsd/index.ts +47 -3
- package/dist/resources/extensions/gsd/metrics.ts +44 -0
- package/dist/resources/extensions/gsd/native-git-bridge.ts +5 -0
- package/dist/resources/extensions/gsd/native-parser-bridge.ts +5 -0
- package/dist/resources/extensions/gsd/paths.ts +9 -0
- package/dist/resources/extensions/gsd/preferences.ts +181 -2
- package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/dist/resources/extensions/gsd/prompts/system.md +2 -0
- package/dist/resources/extensions/gsd/queue-order.ts +231 -0
- package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/dist/resources/extensions/gsd/routing-history.ts +290 -0
- package/dist/resources/extensions/gsd/state.ts +15 -3
- package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/dist/resources/extensions/gsd/tests/budget-prediction.test.ts +220 -0
- package/dist/resources/extensions/gsd/tests/complexity-routing.test.ts +294 -0
- package/dist/resources/extensions/gsd/tests/context-compression.test.ts +180 -0
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +132 -0
- package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +28 -0
- package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/dist/resources/extensions/gsd/tests/routing-history.test.ts +87 -0
- package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +130 -0
- package/dist/resources/extensions/gsd/tests/token-profile.test.ts +263 -0
- package/dist/resources/extensions/gsd/types.ts +28 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/dist/resources/extensions/gsd/worktree.ts +24 -2
- package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
- package/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +493 -13
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +422 -62
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-shared.d.ts +12 -0
- package/packages/pi-ai/dist/providers/google-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google-shared.js +9 -22
- package/packages/pi-ai/dist/providers/google-shared.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-shared.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/google-shared.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/google-shared.test.js +125 -0
- package/packages/pi-ai/dist/providers/google-shared.test.js.map +1 -0
- package/packages/pi-ai/src/models.generated.ts +422 -62
- package/packages/pi-ai/src/providers/google-shared.test.ts +137 -0
- package/packages/pi-ai/src/providers/google-shared.ts +10 -19
- package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +21 -0
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
- package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
- package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts +7 -7
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +209 -13
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +67 -0
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/index.d.ts +5 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +4 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +17 -2
- package/packages/pi-coding-agent/dist/main.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +10 -0
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
- package/packages/pi-coding-agent/src/cli/args.ts +21 -0
- package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
- package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
- package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
- package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
- package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
- package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
- package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
- package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +85 -0
- package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +245 -17
- package/packages/pi-coding-agent/src/index.ts +5 -0
- package/packages/pi-coding-agent/src/main.ts +19 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
- package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +13 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/pkg/dist/modes/interactive/theme/theme.js +10 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
- package/src/resources/extensions/gsd/activity-log.ts +37 -7
- package/src/resources/extensions/gsd/auto-dashboard.ts +4 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +9 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +91 -42
- package/src/resources/extensions/gsd/auto-recovery.ts +7 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/src/resources/extensions/gsd/auto.ts +177 -25
- package/src/resources/extensions/gsd/commands.ts +264 -23
- package/src/resources/extensions/gsd/complexity.ts +236 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/src/resources/extensions/gsd/docs/preferences-reference.md +202 -2
- package/src/resources/extensions/gsd/files.ts +129 -3
- package/src/resources/extensions/gsd/git-service.ts +19 -8
- package/src/resources/extensions/gsd/gitignore.ts +41 -2
- package/src/resources/extensions/gsd/guided-flow.ts +247 -10
- package/src/resources/extensions/gsd/index.ts +47 -3
- package/src/resources/extensions/gsd/metrics.ts +44 -0
- package/src/resources/extensions/gsd/native-git-bridge.ts +5 -0
- package/src/resources/extensions/gsd/native-parser-bridge.ts +5 -0
- package/src/resources/extensions/gsd/paths.ts +9 -0
- package/src/resources/extensions/gsd/preferences.ts +181 -2
- package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/src/resources/extensions/gsd/prompts/system.md +2 -0
- package/src/resources/extensions/gsd/queue-order.ts +231 -0
- package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/src/resources/extensions/gsd/routing-history.ts +290 -0
- package/src/resources/extensions/gsd/state.ts +15 -3
- package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/src/resources/extensions/gsd/templates/preferences.md +14 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/budget-prediction.test.ts +220 -0
- package/src/resources/extensions/gsd/tests/complexity-routing.test.ts +294 -0
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +132 -0
- package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +28 -0
- package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/src/resources/extensions/gsd/tests/routing-history.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +130 -0
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +263 -0
- package/src/resources/extensions/gsd/types.ts +28 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
- package/src/resources/extensions/gsd/worktree.ts +24 -2
- package/src/resources/extensions/shared/next-action-ui.ts +16 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
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.");
|
|
@@ -1131,3 +1252,61 @@ export function resolvePreDispatchHooks(): PreDispatchHookConfig[] {
|
|
|
1131
1252
|
return (prefs?.preferences.pre_dispatch_hooks ?? [])
|
|
1132
1253
|
.filter(h => h.enabled !== false);
|
|
1133
1254
|
}
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* Validate a model ID string.
|
|
1258
|
+
* Returns true if the ID looks like a valid model identifier.
|
|
1259
|
+
*/
|
|
1260
|
+
export function validateModelId(modelId: string): boolean {
|
|
1261
|
+
if (!modelId || typeof modelId !== "string") return false;
|
|
1262
|
+
const trimmed = modelId.trim();
|
|
1263
|
+
if (trimmed.length === 0 || trimmed.length > 256) return false;
|
|
1264
|
+
// Allow alphanumeric, hyphens, underscores, dots, slashes, colons
|
|
1265
|
+
return /^[a-zA-Z0-9\-_./:]+$/.test(trimmed);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Update the models section of the global GSD preferences file.
|
|
1270
|
+
* Performs a safe read-modify-write: reads current content, updates the models
|
|
1271
|
+
* YAML block, and writes back. Creates the file if it doesn't exist.
|
|
1272
|
+
*/
|
|
1273
|
+
export function updatePreferencesModels(models: GSDModelConfigV2): void {
|
|
1274
|
+
const prefsPath = getGlobalGSDPreferencesPath();
|
|
1275
|
+
|
|
1276
|
+
let content = "";
|
|
1277
|
+
if (existsSync(prefsPath)) {
|
|
1278
|
+
content = readFileSync(prefsPath, "utf-8");
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Build the new models block
|
|
1282
|
+
const lines: string[] = ["models:"];
|
|
1283
|
+
for (const [phase, value] of Object.entries(models)) {
|
|
1284
|
+
if (typeof value === "string") {
|
|
1285
|
+
lines.push(` ${phase}: ${value}`);
|
|
1286
|
+
} else if (value && typeof value === "object") {
|
|
1287
|
+
const config = value as GSDPhaseModelConfig;
|
|
1288
|
+
lines.push(` ${phase}:`);
|
|
1289
|
+
lines.push(` model: ${config.model}`);
|
|
1290
|
+
if (config.provider) {
|
|
1291
|
+
lines.push(` provider: ${config.provider}`);
|
|
1292
|
+
}
|
|
1293
|
+
if (config.fallbacks && config.fallbacks.length > 0) {
|
|
1294
|
+
lines.push(` fallbacks:`);
|
|
1295
|
+
for (const fb of config.fallbacks) {
|
|
1296
|
+
lines.push(` - ${fb}`);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
const modelsBlock = lines.join("\n");
|
|
1302
|
+
|
|
1303
|
+
// Replace existing models block or append
|
|
1304
|
+
const modelsRegex = /^models:[\s\S]*?(?=\n[a-z_]|\n*$)/m;
|
|
1305
|
+
if (modelsRegex.test(content)) {
|
|
1306
|
+
content = content.replace(modelsRegex, modelsBlock);
|
|
1307
|
+
} else {
|
|
1308
|
+
content = content.trimEnd() + "\n\n" + modelsBlock + "\n";
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
writeFileSync(prefsPath, content, "utf-8");
|
|
1312
|
+
}
|
|
@@ -54,11 +54,12 @@ Then:
|
|
|
54
54
|
- Don't fix symptoms. Understand *why* something fails before changing code. A test that passes after a change you don't understand is luck, not a fix.
|
|
55
55
|
11. **Blocker discovery:** If execution reveals that the remaining slice plan is fundamentally invalid — not just a bug or minor deviation, but a plan-invalidating finding like a wrong API, missing capability, or architectural mismatch — set `blocker_discovered: true` in the task summary frontmatter and describe the blocker clearly in the summary narrative. Do NOT set `blocker_discovered: true` for ordinary debugging, minor deviations, or issues that can be fixed within the current task or the remaining plan. This flag triggers an automatic replan of the slice.
|
|
56
56
|
12. If you made an architectural, pattern, library, or observability decision during this task that downstream work should know about, append it to `.gsd/DECISIONS.md` (use the **Decisions** output template from the inlined templates below if the file doesn't exist yet). Not every task produces decisions — only append when a meaningful choice was made.
|
|
57
|
-
13.
|
|
58
|
-
14.
|
|
59
|
-
15.
|
|
60
|
-
16.
|
|
61
|
-
17.
|
|
57
|
+
13. If you discover a non-obvious rule, recurring gotcha, or useful pattern during execution, append it to `.gsd/KNOWLEDGE.md`. Only add entries that would save future agents from repeating your investigation. Don't add obvious things.
|
|
58
|
+
14. Use the **Task Summary** output template from the inlined templates below
|
|
59
|
+
15. Write `{{taskSummaryPath}}`
|
|
60
|
+
16. Mark {{taskId}} done in `{{planPath}}` (change `[ ]` to `[x]`)
|
|
61
|
+
17. Do not commit manually — the system auto-commits your changes after this unit completes.
|
|
62
|
+
18. Update `.gsd/STATE.md`
|
|
62
63
|
|
|
63
64
|
All work stays in your working directory: `{{workingDirectory}}`.
|
|
64
65
|
|
|
@@ -65,6 +65,7 @@ Titles live inside file content (headings, frontmatter), not in file or director
|
|
|
65
65
|
PROJECT.md (living doc - what the project is right now)
|
|
66
66
|
REQUIREMENTS.md (requirement contract - tracks active/validated/deferred/out-of-scope)
|
|
67
67
|
DECISIONS.md (append-only register of architectural and pattern decisions)
|
|
68
|
+
KNOWLEDGE.md (append-only register of project-specific rules, patterns, and lessons learned)
|
|
68
69
|
OVERRIDES.md (user-issued overrides that supersede plan content via /gsd steer)
|
|
69
70
|
QUEUE.md (append-only log of queued milestones via /gsd queue)
|
|
70
71
|
STATE.md
|
|
@@ -100,6 +101,7 @@ All auto-mode work happens inside a worktree at `.gsd/worktrees/<MID>/`. This is
|
|
|
100
101
|
- **PROJECT.md** is a living document describing what the project is right now - current state only, updated at slice completion when stale
|
|
101
102
|
- **REQUIREMENTS.md** tracks the requirement contract — requirements move between Active, Validated, Deferred, Blocked, and Out of Scope as slices prove or invalidate them. Update at slice completion when evidence supports a status change.
|
|
102
103
|
- **DECISIONS.md** is an append-only register of architectural and pattern decisions - read it during planning/research, append to it during execution when a meaningful decision is made
|
|
104
|
+
- **KNOWLEDGE.md** is an append-only register of project-specific rules, patterns, and lessons learned. Read it at the start of every unit. Append to it when you discover a recurring issue, a non-obvious pattern, or a rule that future agents should follow.
|
|
103
105
|
- **CONTEXT.md** files (milestone or slice level) capture the brief — scope, goals, constraints, and key decisions from discussion. When present, they are the authoritative source for what a milestone or slice is trying to achieve. Read them before planning or executing.
|
|
104
106
|
- **Milestones** are major project phases (M001, M002, ...)
|
|
105
107
|
- **Slices** are demoable vertical increments (S01, S02, ...) ordered by risk. After each slice completes, the roadmap is reassessed before the next slice begins.
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Queue Order — Custom milestone execution ordering.
|
|
3
|
+
*
|
|
4
|
+
* Stores an explicit execution order in `.gsd/QUEUE-ORDER.json`.
|
|
5
|
+
* When present, `findMilestoneIds()` uses this order instead of
|
|
6
|
+
* the default numeric sort (milestoneIdSort).
|
|
7
|
+
*
|
|
8
|
+
* The file is committed to git (not gitignored) so ordering
|
|
9
|
+
* survives branch switches and is shared across sessions.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { gsdRoot } from "./paths.js";
|
|
15
|
+
import { milestoneIdSort } from "./guided-flow.js";
|
|
16
|
+
|
|
17
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
interface QueueOrderFile {
|
|
20
|
+
order: string[];
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DependencyViolation {
|
|
25
|
+
milestone: string;
|
|
26
|
+
dependsOn: string;
|
|
27
|
+
type: 'would_block' | 'circular' | 'missing_dep';
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DependencyRedundancy {
|
|
32
|
+
milestone: string;
|
|
33
|
+
dependsOn: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DependencyValidation {
|
|
37
|
+
valid: boolean;
|
|
38
|
+
violations: DependencyViolation[];
|
|
39
|
+
redundant: DependencyRedundancy[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── Path ────────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function queueOrderPath(basePath: string): string {
|
|
45
|
+
return join(gsdRoot(basePath), "QUEUE-ORDER.json");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Read / Write ────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Load the custom queue order. Returns null if no file exists or if
|
|
52
|
+
* the file is corrupt/unreadable.
|
|
53
|
+
*/
|
|
54
|
+
export function loadQueueOrder(basePath: string): string[] | null {
|
|
55
|
+
const p = queueOrderPath(basePath);
|
|
56
|
+
if (!existsSync(p)) return null;
|
|
57
|
+
try {
|
|
58
|
+
const data: QueueOrderFile = JSON.parse(readFileSync(p, "utf-8"));
|
|
59
|
+
if (!Array.isArray(data.order)) return null;
|
|
60
|
+
return data.order;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Save a custom queue order to disk.
|
|
68
|
+
*/
|
|
69
|
+
export function saveQueueOrder(basePath: string, order: string[]): void {
|
|
70
|
+
const data: QueueOrderFile = {
|
|
71
|
+
order,
|
|
72
|
+
updatedAt: new Date().toISOString(),
|
|
73
|
+
};
|
|
74
|
+
writeFileSync(queueOrderPath(basePath), JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── Sorting ─────────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Sort milestone IDs respecting a custom order.
|
|
81
|
+
*
|
|
82
|
+
* - IDs present in `customOrder` appear in that exact sequence.
|
|
83
|
+
* - IDs on disk but NOT in `customOrder` are appended at the end,
|
|
84
|
+
* sorted by the default `milestoneIdSort` (numeric).
|
|
85
|
+
* - IDs in `customOrder` but NOT on disk are silently skipped.
|
|
86
|
+
* - When `customOrder` is null, falls back to `milestoneIdSort`.
|
|
87
|
+
*/
|
|
88
|
+
export function sortByQueueOrder(ids: string[], customOrder: string[] | null): string[] {
|
|
89
|
+
if (!customOrder) return [...ids].sort(milestoneIdSort);
|
|
90
|
+
|
|
91
|
+
const idSet = new Set(ids);
|
|
92
|
+
const ordered: string[] = [];
|
|
93
|
+
|
|
94
|
+
// First: IDs from customOrder that exist on disk
|
|
95
|
+
for (const id of customOrder) {
|
|
96
|
+
if (idSet.has(id)) {
|
|
97
|
+
ordered.push(id);
|
|
98
|
+
idSet.delete(id);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Then: remaining IDs not in customOrder, in default sort order
|
|
103
|
+
const remaining = [...idSet].sort(milestoneIdSort);
|
|
104
|
+
return [...ordered, ...remaining];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── Pruning ─────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Remove IDs from the queue order file that are no longer valid
|
|
111
|
+
* (completed or deleted milestones). No-op if file doesn't exist.
|
|
112
|
+
*/
|
|
113
|
+
export function pruneQueueOrder(basePath: string, validIds: string[]): void {
|
|
114
|
+
const order = loadQueueOrder(basePath);
|
|
115
|
+
if (!order) return;
|
|
116
|
+
|
|
117
|
+
const validSet = new Set(validIds);
|
|
118
|
+
const pruned = order.filter(id => validSet.has(id));
|
|
119
|
+
|
|
120
|
+
if (pruned.length !== order.length) {
|
|
121
|
+
saveQueueOrder(basePath, pruned);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── Validation ──────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Validate a proposed queue order against dependency constraints.
|
|
129
|
+
*
|
|
130
|
+
* Checks:
|
|
131
|
+
* - would_block: A milestone is placed before one of its dependencies
|
|
132
|
+
* - circular: Two or more milestones form a dependency cycle
|
|
133
|
+
* - missing_dep: A milestone depends on an ID that doesn't exist
|
|
134
|
+
* - redundant: A dependency is satisfied by queue position (dep comes earlier)
|
|
135
|
+
*/
|
|
136
|
+
export function validateQueueOrder(
|
|
137
|
+
order: string[],
|
|
138
|
+
depsMap: Map<string, string[]>,
|
|
139
|
+
completedIds: Set<string>,
|
|
140
|
+
): DependencyValidation {
|
|
141
|
+
const violations: DependencyViolation[] = [];
|
|
142
|
+
const redundant: DependencyRedundancy[] = [];
|
|
143
|
+
|
|
144
|
+
const positionMap = new Map<string, number>();
|
|
145
|
+
for (let i = 0; i < order.length; i++) {
|
|
146
|
+
positionMap.set(order[i], i);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const allKnownIds = new Set([...order, ...completedIds]);
|
|
150
|
+
|
|
151
|
+
for (const [mid, deps] of depsMap) {
|
|
152
|
+
const midPos = positionMap.get(mid);
|
|
153
|
+
if (midPos === undefined) continue; // not in pending order
|
|
154
|
+
|
|
155
|
+
for (const dep of deps) {
|
|
156
|
+
// Dep already completed — always satisfied
|
|
157
|
+
if (completedIds.has(dep)) continue;
|
|
158
|
+
|
|
159
|
+
// Dep doesn't exist anywhere
|
|
160
|
+
if (!allKnownIds.has(dep)) {
|
|
161
|
+
violations.push({
|
|
162
|
+
milestone: mid,
|
|
163
|
+
dependsOn: dep,
|
|
164
|
+
type: 'missing_dep',
|
|
165
|
+
message: `${mid} depends on ${dep}, but ${dep} does not exist.`,
|
|
166
|
+
});
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const depPos = positionMap.get(dep);
|
|
171
|
+
if (depPos === undefined) continue; // dep not in pending order (edge case)
|
|
172
|
+
|
|
173
|
+
if (depPos > midPos) {
|
|
174
|
+
// Dep comes AFTER this milestone in the order — violation
|
|
175
|
+
violations.push({
|
|
176
|
+
milestone: mid,
|
|
177
|
+
dependsOn: dep,
|
|
178
|
+
type: 'would_block',
|
|
179
|
+
message: `${mid} cannot run before ${dep} — ${mid} depends_on: [${dep}].`,
|
|
180
|
+
});
|
|
181
|
+
} else {
|
|
182
|
+
// Dep comes before — satisfied by position, redundant
|
|
183
|
+
redundant.push({ milestone: mid, dependsOn: dep });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check for circular dependencies
|
|
189
|
+
const visited = new Set<string>();
|
|
190
|
+
const inStack = new Set<string>();
|
|
191
|
+
|
|
192
|
+
function hasCycle(node: string, path: string[]): string[] | null {
|
|
193
|
+
if (inStack.has(node)) return [...path, node];
|
|
194
|
+
if (visited.has(node)) return null;
|
|
195
|
+
|
|
196
|
+
visited.add(node);
|
|
197
|
+
inStack.add(node);
|
|
198
|
+
|
|
199
|
+
const deps = depsMap.get(node) ?? [];
|
|
200
|
+
for (const dep of deps) {
|
|
201
|
+
if (completedIds.has(dep)) continue;
|
|
202
|
+
const cycle = hasCycle(dep, [...path, node]);
|
|
203
|
+
if (cycle) return cycle;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
inStack.delete(node);
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const mid of order) {
|
|
211
|
+
if (!visited.has(mid)) {
|
|
212
|
+
const cycle = hasCycle(mid, []);
|
|
213
|
+
if (cycle) {
|
|
214
|
+
const cycleStr = cycle.join(' → ');
|
|
215
|
+
violations.push({
|
|
216
|
+
milestone: cycle[0],
|
|
217
|
+
dependsOn: cycle[cycle.length - 2],
|
|
218
|
+
type: 'circular',
|
|
219
|
+
message: `Circular dependency: ${cycleStr}`,
|
|
220
|
+
});
|
|
221
|
+
break; // one cycle report is enough
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
valid: violations.length === 0,
|
|
228
|
+
violations,
|
|
229
|
+
redundant,
|
|
230
|
+
};
|
|
231
|
+
}
|