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.
Files changed (225) hide show
  1. package/README.md +39 -0
  2. package/dist/onboarding.js +2 -2
  3. package/dist/remote-questions-config.d.ts +10 -0
  4. package/dist/remote-questions-config.js +36 -0
  5. package/dist/resources/extensions/gsd/activity-log.ts +37 -7
  6. package/dist/resources/extensions/gsd/auto-dashboard.ts +4 -0
  7. package/dist/resources/extensions/gsd/auto-dispatch.ts +9 -3
  8. package/dist/resources/extensions/gsd/auto-prompts.ts +91 -42
  9. package/dist/resources/extensions/gsd/auto-recovery.ts +7 -2
  10. package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
  11. package/dist/resources/extensions/gsd/auto.ts +177 -25
  12. package/dist/resources/extensions/gsd/commands.ts +264 -23
  13. package/dist/resources/extensions/gsd/complexity.ts +236 -0
  14. package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
  15. package/dist/resources/extensions/gsd/docs/preferences-reference.md +202 -2
  16. package/dist/resources/extensions/gsd/files.ts +129 -3
  17. package/dist/resources/extensions/gsd/git-service.ts +19 -8
  18. package/dist/resources/extensions/gsd/gitignore.ts +41 -2
  19. package/dist/resources/extensions/gsd/guided-flow.ts +247 -10
  20. package/dist/resources/extensions/gsd/index.ts +47 -3
  21. package/dist/resources/extensions/gsd/metrics.ts +44 -0
  22. package/dist/resources/extensions/gsd/native-git-bridge.ts +5 -0
  23. package/dist/resources/extensions/gsd/native-parser-bridge.ts +5 -0
  24. package/dist/resources/extensions/gsd/paths.ts +9 -0
  25. package/dist/resources/extensions/gsd/preferences.ts +181 -2
  26. package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
  27. package/dist/resources/extensions/gsd/prompts/system.md +2 -0
  28. package/dist/resources/extensions/gsd/queue-order.ts +231 -0
  29. package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  30. package/dist/resources/extensions/gsd/routing-history.ts +290 -0
  31. package/dist/resources/extensions/gsd/state.ts +15 -3
  32. package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
  33. package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
  34. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -0
  35. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  36. package/dist/resources/extensions/gsd/tests/budget-prediction.test.ts +220 -0
  37. package/dist/resources/extensions/gsd/tests/complexity-routing.test.ts +294 -0
  38. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +180 -0
  39. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  40. package/dist/resources/extensions/gsd/tests/git-service.test.ts +132 -0
  41. package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  42. package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  43. package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  44. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +28 -0
  45. package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  46. package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  47. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  48. package/dist/resources/extensions/gsd/tests/routing-history.test.ts +87 -0
  49. package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  50. package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +130 -0
  51. package/dist/resources/extensions/gsd/tests/token-profile.test.ts +263 -0
  52. package/dist/resources/extensions/gsd/types.ts +28 -0
  53. package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
  54. package/dist/resources/extensions/gsd/worktree.ts +24 -2
  55. package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
  56. package/package.json +1 -1
  57. package/packages/pi-ai/dist/models.generated.d.ts +493 -13
  58. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  59. package/packages/pi-ai/dist/models.generated.js +422 -62
  60. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  61. package/packages/pi-ai/dist/providers/google-shared.d.ts +12 -0
  62. package/packages/pi-ai/dist/providers/google-shared.d.ts.map +1 -1
  63. package/packages/pi-ai/dist/providers/google-shared.js +9 -22
  64. package/packages/pi-ai/dist/providers/google-shared.js.map +1 -1
  65. package/packages/pi-ai/dist/providers/google-shared.test.d.ts +2 -0
  66. package/packages/pi-ai/dist/providers/google-shared.test.d.ts.map +1 -0
  67. package/packages/pi-ai/dist/providers/google-shared.test.js +125 -0
  68. package/packages/pi-ai/dist/providers/google-shared.test.js.map +1 -0
  69. package/packages/pi-ai/src/models.generated.ts +422 -62
  70. package/packages/pi-ai/src/providers/google-shared.test.ts +137 -0
  71. package/packages/pi-ai/src/providers/google-shared.ts +10 -19
  72. package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
  73. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  74. package/packages/pi-coding-agent/dist/cli/args.js +21 -0
  75. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  76. package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
  77. package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
  78. package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
  79. package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
  81. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
  82. package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
  83. package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
  84. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
  85. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
  86. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
  87. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
  88. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
  89. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
  90. package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
  91. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
  92. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
  93. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
  94. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
  95. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
  96. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
  97. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
  98. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
  99. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
  100. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
  101. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  102. package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
  103. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
  105. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
  106. package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
  107. package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
  108. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
  109. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
  110. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
  111. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
  112. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
  113. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
  115. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  117. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  118. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  119. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts +7 -7
  120. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +209 -13
  122. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.d.ts +2 -0
  124. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.d.ts.map +1 -0
  125. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +67 -0
  126. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -0
  127. package/packages/pi-coding-agent/dist/index.d.ts +5 -1
  128. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/index.js +4 -1
  130. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  131. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/main.js +17 -2
  133. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
  135. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
  137. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
  138. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
  139. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  140. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
  141. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
  142. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
  143. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
  144. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
  145. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  146. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
  147. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  148. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  149. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +10 -0
  150. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
  151. package/packages/pi-coding-agent/src/cli/args.ts +21 -0
  152. package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
  153. package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
  154. package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
  155. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
  156. package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
  157. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
  158. package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
  159. package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
  160. package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
  161. package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
  162. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  163. package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +85 -0
  164. package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +245 -17
  165. package/packages/pi-coding-agent/src/index.ts +5 -0
  166. package/packages/pi-coding-agent/src/main.ts +19 -2
  167. package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
  168. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
  169. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
  170. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
  171. package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +13 -0
  172. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  173. package/pkg/dist/modes/interactive/theme/theme.js +10 -0
  174. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
  175. package/src/resources/extensions/gsd/activity-log.ts +37 -7
  176. package/src/resources/extensions/gsd/auto-dashboard.ts +4 -0
  177. package/src/resources/extensions/gsd/auto-dispatch.ts +9 -3
  178. package/src/resources/extensions/gsd/auto-prompts.ts +91 -42
  179. package/src/resources/extensions/gsd/auto-recovery.ts +7 -2
  180. package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
  181. package/src/resources/extensions/gsd/auto.ts +177 -25
  182. package/src/resources/extensions/gsd/commands.ts +264 -23
  183. package/src/resources/extensions/gsd/complexity.ts +236 -0
  184. package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
  185. package/src/resources/extensions/gsd/docs/preferences-reference.md +202 -2
  186. package/src/resources/extensions/gsd/files.ts +129 -3
  187. package/src/resources/extensions/gsd/git-service.ts +19 -8
  188. package/src/resources/extensions/gsd/gitignore.ts +41 -2
  189. package/src/resources/extensions/gsd/guided-flow.ts +247 -10
  190. package/src/resources/extensions/gsd/index.ts +47 -3
  191. package/src/resources/extensions/gsd/metrics.ts +44 -0
  192. package/src/resources/extensions/gsd/native-git-bridge.ts +5 -0
  193. package/src/resources/extensions/gsd/native-parser-bridge.ts +5 -0
  194. package/src/resources/extensions/gsd/paths.ts +9 -0
  195. package/src/resources/extensions/gsd/preferences.ts +181 -2
  196. package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
  197. package/src/resources/extensions/gsd/prompts/system.md +2 -0
  198. package/src/resources/extensions/gsd/queue-order.ts +231 -0
  199. package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  200. package/src/resources/extensions/gsd/routing-history.ts +290 -0
  201. package/src/resources/extensions/gsd/state.ts +15 -3
  202. package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
  203. package/src/resources/extensions/gsd/templates/preferences.md +14 -0
  204. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -0
  205. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  206. package/src/resources/extensions/gsd/tests/budget-prediction.test.ts +220 -0
  207. package/src/resources/extensions/gsd/tests/complexity-routing.test.ts +294 -0
  208. package/src/resources/extensions/gsd/tests/context-compression.test.ts +180 -0
  209. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  210. package/src/resources/extensions/gsd/tests/git-service.test.ts +132 -0
  211. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  212. package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  213. package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  214. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +28 -0
  215. package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  216. package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  217. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  218. package/src/resources/extensions/gsd/tests/routing-history.test.ts +87 -0
  219. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  220. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +130 -0
  221. package/src/resources/extensions/gsd/tests/token-profile.test.ts +263 -0
  222. package/src/resources/extensions/gsd/types.ts +28 -0
  223. package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
  224. package/src/resources/extensions/gsd/worktree.ts +24 -2
  225. 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. Use the **Task Summary** output template from the inlined templates below
58
- 14. Write `{{taskSummaryPath}}`
59
- 15. Mark {{taskId}} done in `{{planPath}}` (change `[ ]` to `[x]`)
60
- 16. Do not commit manually the system auto-commits your changes after this unit completes.
61
- 17. Update `.gsd/STATE.md`
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
+ }