gsd-opencode 1.20.1 → 1.20.3

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 (31) hide show
  1. package/commands/gsd/gsd-check-profile.md +30 -0
  2. package/commands/gsd/gsd-research-phase.md +2 -2
  3. package/get-shit-done/bin/gsd-oc-commands/check-oc-config-json.cjs +169 -0
  4. package/get-shit-done/bin/gsd-oc-commands/check-opencode-json.cjs +86 -0
  5. package/get-shit-done/bin/gsd-oc-commands/get-profile.cjs +117 -0
  6. package/get-shit-done/bin/gsd-oc-commands/set-profile.cjs +357 -0
  7. package/get-shit-done/bin/gsd-oc-commands/update-opencode-json.cjs +199 -0
  8. package/get-shit-done/bin/gsd-oc-commands/validate-models.cjs +75 -0
  9. package/get-shit-done/bin/gsd-oc-lib/oc-config.cjs +205 -0
  10. package/get-shit-done/bin/gsd-oc-lib/oc-core.cjs +113 -0
  11. package/get-shit-done/bin/gsd-oc-lib/oc-models.cjs +133 -0
  12. package/get-shit-done/bin/gsd-oc-lib/oc-profile-config.cjs +409 -0
  13. package/get-shit-done/bin/gsd-oc-tools.cjs +130 -0
  14. package/get-shit-done/bin/lib/oc-config.cjs +200 -0
  15. package/get-shit-done/bin/lib/oc-core.cjs +114 -0
  16. package/get-shit-done/bin/lib/oc-models.cjs +133 -0
  17. package/get-shit-done/bin/test/fixtures/oc-config-invalid.json +14 -0
  18. package/get-shit-done/bin/test/fixtures/oc-config-valid.json +22 -0
  19. package/get-shit-done/bin/test/get-profile.test.cjs +447 -0
  20. package/get-shit-done/bin/test/oc-profile-config.test.cjs +377 -0
  21. package/get-shit-done/bin/test/pivot-profile.test.cjs +276 -0
  22. package/get-shit-done/bin/test/set-profile.test.cjs +301 -0
  23. package/get-shit-done/workflows/diagnose-issues.md +1 -1
  24. package/get-shit-done/workflows/discuss-phase.md +1 -1
  25. package/get-shit-done/workflows/new-project.md +4 -4
  26. package/get-shit-done/workflows/oc-check-profile.md +181 -0
  27. package/get-shit-done/workflows/oc-set-profile.md +83 -243
  28. package/get-shit-done/workflows/plan-phase.md +4 -4
  29. package/get-shit-done/workflows/quick.md +1 -1
  30. package/get-shit-done/workflows/settings.md +4 -3
  31. package/package.json +2 -2
@@ -0,0 +1,205 @@
1
+ /**
2
+ * oc-config.cjs — Profile configuration operations for gsd-oc-tools CLI
3
+ *
4
+ * Provides functions for loading profile config and applying model assignments to opencode.json.
5
+ * Follows gsd-tools.cjs architecture pattern.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ /**
12
+ * Valid profile types whitelist
13
+ */
14
+ const VALID_PROFILES = ['simple', 'smart', 'genius'];
15
+
16
+ /**
17
+ * Profile to agent mapping
18
+ * Maps profile keys to opencode.json agent names
19
+ */
20
+ const PROFILE_AGENT_MAPPING = {
21
+ // Planning agents
22
+ planning: [
23
+ 'gsd-planner',
24
+ 'gsd-plan-checker',
25
+ 'gsd-phase-researcher',
26
+ 'gsd-roadmapper',
27
+ 'gsd-project-researcher',
28
+ 'gsd-research-synthesizer',
29
+ 'gsd-codebase-mapper'
30
+ ],
31
+ // Execution agents
32
+ execution: [
33
+ 'gsd-executor',
34
+ 'gsd-debugger'
35
+ ],
36
+ // Verification agents
37
+ verification: [
38
+ 'gsd-verifier',
39
+ 'gsd-integration-checker'
40
+ ]
41
+ };
42
+
43
+ /**
44
+ * Load profile configuration from .planning/config.json
45
+ *
46
+ * @param {string} cwd - Current working directory
47
+ * @returns {Object|null} Parsed config object or null on error
48
+ */
49
+ function loadProfileConfig(cwd) {
50
+ try {
51
+ const configPath = path.join(cwd, '.planning', 'config.json');
52
+
53
+ if (!fs.existsSync(configPath)) {
54
+ return null;
55
+ }
56
+
57
+ const content = fs.readFileSync(configPath, 'utf8');
58
+ let config = JSON.parse(content);
59
+
60
+ // Auto-migrate old key name: current_os_profile → current_oc_profile
61
+ if (config.current_os_profile && !config.current_oc_profile) {
62
+ config.current_oc_profile = config.current_os_profile;
63
+ delete config.current_os_profile;
64
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
65
+ }
66
+
67
+ return config;
68
+ } catch (err) {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Apply profile configuration to opencode.json
75
+ * Updates agent model assignments based on profile
76
+ *
77
+ * @param {string} opencodePath - Path to opencode.json
78
+ * @param {string} configPath - Path to .planning/config.json
79
+ * @param {string} [profileName] - Optional profile name to use (overrides current_oc_profile)
80
+ * @returns {Object} {success: true, updated: [agentNames]} or {success: false, error: {code, message}}
81
+ */
82
+ function applyProfileToOpencode(opencodePath, configPath, profileName = null) {
83
+ try {
84
+ // Load profile config
85
+ let config;
86
+ if (fs.existsSync(configPath)) {
87
+ const content = fs.readFileSync(configPath, 'utf8');
88
+ config = JSON.parse(content);
89
+ } else {
90
+ return {
91
+ success: false,
92
+ error: {
93
+ code: 'CONFIG_NOT_FOUND',
94
+ message: `.planning/config.json not found at ${configPath}`
95
+ }
96
+ };
97
+ }
98
+
99
+ // Determine which profile to use
100
+ const targetProfile = profileName || config.current_oc_profile;
101
+
102
+ if (!targetProfile) {
103
+ return {
104
+ success: false,
105
+ error: {
106
+ code: 'PROFILE_NOT_FOUND',
107
+ message: 'current_oc_profile not found in config.json. Run set-profile with a profile name first.'
108
+ }
109
+ };
110
+ }
111
+
112
+ // Validate profile exists in profiles.presets
113
+ const presets = config.profiles?.presets;
114
+ if (!presets || !presets[targetProfile]) {
115
+ const availableProfiles = presets ? Object.keys(presets).join(', ') : 'none';
116
+ return {
117
+ success: false,
118
+ error: {
119
+ code: 'PROFILE_NOT_FOUND',
120
+ message: `Profile "${targetProfile}" not found in profiles.presets. Available profiles: ${availableProfiles}`
121
+ }
122
+ };
123
+ }
124
+
125
+ // Load or create opencode.json
126
+ let opencodeData;
127
+ if (!fs.existsSync(opencodePath)) {
128
+ // Create initial opencode.json structure
129
+ opencodeData = {
130
+ "$schema": "https://opencode.ai/config.json",
131
+ "agent": {}
132
+ };
133
+ } else {
134
+ // Load existing opencode.json
135
+ const opencodeContent = fs.readFileSync(opencodePath, 'utf8');
136
+ opencodeData = JSON.parse(opencodeContent);
137
+
138
+ // Ensure agent object exists
139
+ if (!opencodeData.agent) {
140
+ opencodeData.agent = {};
141
+ }
142
+ }
143
+
144
+ // Get model assignments from profiles.presets.{profile_name}.models
145
+ const profileModels = presets[targetProfile];
146
+
147
+ if (!profileModels.planning && !profileModels.execution && !profileModels.verification) {
148
+ return {
149
+ success: false,
150
+ error: {
151
+ code: 'PROFILE_NOT_FOUND',
152
+ message: `No model assignments found for profile "${targetProfile}"`
153
+ }
154
+ };
155
+ }
156
+
157
+ // Apply model assignments to agents (MERGE - preserve non-gsd agents)
158
+ const updatedAgents = [];
159
+
160
+ // Initialize agent object if it doesn't exist
161
+ if (!opencodeData.agent) {
162
+ opencodeData.agent = {};
163
+ }
164
+
165
+ // Apply each profile category - ONLY update gsd-* agents
166
+ for (const [category, agentNames] of Object.entries(PROFILE_AGENT_MAPPING)) {
167
+ const modelId = profileModels[category];
168
+
169
+ if (modelId) {
170
+ for (const agentName of agentNames) {
171
+ // Only update gsd-* agents, preserve all others
172
+ if (typeof opencodeData.agent[agentName] === 'object' && opencodeData.agent[agentName] !== null) {
173
+ opencodeData.agent[agentName].model = modelId;
174
+ } else {
175
+ opencodeData.agent[agentName] = { model: modelId };
176
+ }
177
+ updatedAgents.push({ agent: agentName, model: modelId });
178
+ }
179
+ }
180
+ }
181
+
182
+ // write updated opencode.json
183
+ fs.writeFileSync(opencodePath, JSON.stringify(opencodeData, null, 2) + '\n', 'utf8');
184
+
185
+ return {
186
+ success: true,
187
+ updated: updatedAgents
188
+ };
189
+ } catch (err) {
190
+ return {
191
+ success: false,
192
+ error: {
193
+ code: 'UPDATE_FAILED',
194
+ message: `Failed to apply profile: ${err.message}`
195
+ }
196
+ };
197
+ }
198
+ }
199
+
200
+ module.exports = {
201
+ loadProfileConfig,
202
+ applyProfileToOpencode,
203
+ VALID_PROFILES,
204
+ PROFILE_AGENT_MAPPING
205
+ };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * oc-core.cjs — Shared utilities for gsd-oc-tools CLI
3
+ *
4
+ * Provides common functions for output formatting, error handling, file operations.
5
+ * Follows gsd-tools.cjs architecture pattern.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ /**
12
+ * Output result in JSON envelope format
13
+ * Large payloads (>50KB) are written to temp file with @file: prefix
14
+ *
15
+ * @param {Object} result - The result data to output
16
+ * @param {boolean} raw - If true, output raw value instead of envelope
17
+ * @param {*} rawValue - The raw value to output if raw=true
18
+ */
19
+ function output(result, raw = false, rawValue = null) {
20
+ let outputStr;
21
+
22
+ if (raw && rawValue !== null) {
23
+ // rawValue is already stringified, use it directly
24
+ outputStr = rawValue;
25
+ } else {
26
+ outputStr = JSON.stringify(result, null, 2);
27
+ }
28
+
29
+ // Large payload handling (>50KB)
30
+ if (outputStr.length > 50 * 1024) {
31
+ const tempFile = path.join(require('os').tmpdir(), `gsd-oc-${Date.now()}.json`);
32
+ fs.writeFileSync(tempFile, outputStr, 'utf8');
33
+ console.log(`@file:${tempFile}`);
34
+ } else {
35
+ console.log(outputStr);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Output error in standardized envelope format to stderr
41
+ *
42
+ * @param {string} message - Error message
43
+ * @param {string} code - Error code (e.g., 'CONFIG_NOT_FOUND', 'INVALID_JSON')
44
+ */
45
+ function error(message, code = 'UNKNOWN_ERROR') {
46
+ const errorEnvelope = {
47
+ success: false,
48
+ error: {
49
+ code,
50
+ message
51
+ }
52
+ };
53
+ console.error(JSON.stringify(errorEnvelope, null, 2));
54
+ process.exit(1);
55
+ }
56
+
57
+ /**
58
+ * Safely read a file, returning null on failure
59
+ *
60
+ * @param {string} filePath - Path to file
61
+ * @returns {string|null} File contents or null
62
+ */
63
+ function safeReadFile(filePath) {
64
+ try {
65
+ return fs.readFileSync(filePath, 'utf8');
66
+ } catch (err) {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Create timestamped backup of a file
73
+ *
74
+ * @param {string} filePath - Path to file to backup
75
+ * @param {string} backupDir - Directory for backups (.opencode-backups/)
76
+ * @returns {string|null} Backup file path or null on failure
77
+ */
78
+ function createBackup(filePath, backupDir = '.opencode-backups') {
79
+ try {
80
+ // Ensure backup directory exists
81
+ if (!fs.existsSync(backupDir)) {
82
+ fs.mkdirSync(backupDir, { recursive: true });
83
+ }
84
+
85
+ // read original file
86
+ const content = fs.readFileSync(filePath, 'utf8');
87
+
88
+ // Create timestamped filename (YYYYMMDD-HHmmss-SSS format)
89
+ const now = new Date();
90
+ const timestamp = now.toISOString()
91
+ .replace(/[-:T]/g, '')
92
+ .replace(/\.\d{3}Z$/, '')
93
+ .replace(/(\d{8})(\d{6})(\d{3})/, '$1-$2-$3');
94
+
95
+ const fileName = path.basename(filePath);
96
+ const backupFileName = `${timestamp}-${fileName}`;
97
+ const backupPath = path.join(backupDir, backupFileName);
98
+
99
+ // write backup
100
+ fs.writeFileSync(backupPath, content, 'utf8');
101
+
102
+ return backupPath;
103
+ } catch (err) {
104
+ return null;
105
+ }
106
+ }
107
+
108
+ module.exports = {
109
+ output,
110
+ error,
111
+ safeReadFile,
112
+ createBackup
113
+ };
@@ -0,0 +1,133 @@
1
+ /**
2
+ * oc-models.cjs — Model catalog operations for gsd-oc-tools CLI
3
+ *
4
+ * Provides functions for fetching and validating model IDs against opencode models output.
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { execSync } = require('child_process');
10
+
11
+ /**
12
+ * Fetch model catalog from opencode models command
13
+ *
14
+ * @returns {Object} {success: boolean, models: string[]} or {success: false, error: {...}}
15
+ */
16
+ function getModelCatalog() {
17
+ try {
18
+ const output = execSync('opencode models', {
19
+ encoding: 'utf8',
20
+ stdio: ['pipe', 'pipe', 'pipe']
21
+ });
22
+
23
+ // Parse output (one model per line)
24
+ const models = output
25
+ .split('\n')
26
+ .map(line => line.trim())
27
+ .filter(line => line.length > 0);
28
+
29
+ return {
30
+ success: true,
31
+ models
32
+ };
33
+ } catch (err) {
34
+ return {
35
+ success: false,
36
+ error: {
37
+ code: 'FETCH_FAILED',
38
+ message: `Failed to fetch model catalog: ${err.message}`
39
+ }
40
+ };
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Validate model IDs in opencode.json against valid models list
46
+ *
47
+ * @param {string} opencodePath - Path to opencode.json file
48
+ * @param {string[]} validModels - Array of valid model IDs
49
+ * @returns {Object} {valid, total, validCount, invalidCount, issues: [{agent, model, reason}]}
50
+ */
51
+ function validateModelIds(opencodePath, validModels) {
52
+ const issues = [];
53
+ let total = 0;
54
+ let validCount = 0;
55
+ let invalidCount = 0;
56
+
57
+ try {
58
+ const content = fs.readFileSync(opencodePath, 'utf8');
59
+ const opencodeData = JSON.parse(content);
60
+
61
+ // Look for agent model assignments
62
+ // Common patterns: agent.model, profiles.*.model, models.*
63
+ const assignments = [];
64
+
65
+ // Check for agents at root level
66
+ if (opencodeData.agent && typeof opencodeData.agent === 'object') {
67
+ Object.entries(opencodeData.agent).forEach(([agentName, config]) => {
68
+ if (typeof config === 'string') {
69
+ assignments.push({ agent: `agent.${agentName}`, model: config });
70
+ } else if (config && typeof config === 'object' && config.model) {
71
+ assignments.push({ agent: `agent.${agentName}`, model: config.model });
72
+ }
73
+ });
74
+ }
75
+
76
+ // Check for profiles
77
+ if (opencodeData.profiles && typeof opencodeData.profiles === 'object') {
78
+ Object.entries(opencodeData.profiles).forEach(([profileName, config]) => {
79
+ if (config && typeof config === 'object') {
80
+ Object.entries(config).forEach(([key, value]) => {
81
+ if (key.includes('model') && typeof value === 'string') {
82
+ assignments.push({ agent: `profiles.${profileName}.${key}`, model: value });
83
+ }
84
+ });
85
+ }
86
+ });
87
+ }
88
+
89
+ // Check for models at root level
90
+ if (opencodeData.models && typeof opencodeData.models === 'object') {
91
+ Object.entries(opencodeData.models).forEach(([modelName, modelId]) => {
92
+ if (typeof modelId === 'string') {
93
+ assignments.push({ agent: `models.${modelName}`, model: modelId });
94
+ }
95
+ });
96
+ }
97
+
98
+ // Validate each assignment
99
+ total = assignments.length;
100
+ for (const { agent, model } of assignments) {
101
+ if (validModels.includes(model)) {
102
+ validCount++;
103
+ } else {
104
+ invalidCount++;
105
+ issues.push({
106
+ agent,
107
+ model,
108
+ reason: 'Model ID not found in opencode models catalog'
109
+ });
110
+ }
111
+ }
112
+
113
+ return {
114
+ valid: invalidCount === 0,
115
+ total,
116
+ validCount,
117
+ invalidCount,
118
+ issues
119
+ };
120
+ } catch (err) {
121
+ if (err.code === 'ENOENT') {
122
+ throw new Error('CONFIG_NOT_FOUND');
123
+ } else if (err instanceof SyntaxError) {
124
+ throw new Error('INVALID_JSON');
125
+ }
126
+ throw err;
127
+ }
128
+ }
129
+
130
+ module.exports = {
131
+ getModelCatalog,
132
+ validateModelIds
133
+ };