gsd-opencode 1.20.2 → 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 (25) hide show
  1. package/commands/gsd/gsd-check-profile.md +30 -0
  2. package/get-shit-done/bin/gsd-oc-commands/check-oc-config-json.cjs +169 -0
  3. package/get-shit-done/bin/gsd-oc-commands/check-opencode-json.cjs +86 -0
  4. package/get-shit-done/bin/gsd-oc-commands/get-profile.cjs +117 -0
  5. package/get-shit-done/bin/gsd-oc-commands/set-profile.cjs +357 -0
  6. package/get-shit-done/bin/gsd-oc-commands/update-opencode-json.cjs +199 -0
  7. package/get-shit-done/bin/gsd-oc-commands/validate-models.cjs +75 -0
  8. package/get-shit-done/bin/gsd-oc-lib/oc-config.cjs +205 -0
  9. package/get-shit-done/bin/gsd-oc-lib/oc-core.cjs +113 -0
  10. package/get-shit-done/bin/gsd-oc-lib/oc-models.cjs +133 -0
  11. package/get-shit-done/bin/gsd-oc-lib/oc-profile-config.cjs +409 -0
  12. package/get-shit-done/bin/gsd-oc-tools.cjs +130 -0
  13. package/get-shit-done/bin/lib/oc-config.cjs +200 -0
  14. package/get-shit-done/bin/lib/oc-core.cjs +114 -0
  15. package/get-shit-done/bin/lib/oc-models.cjs +133 -0
  16. package/get-shit-done/bin/test/fixtures/oc-config-invalid.json +14 -0
  17. package/get-shit-done/bin/test/fixtures/oc-config-valid.json +22 -0
  18. package/get-shit-done/bin/test/get-profile.test.cjs +447 -0
  19. package/get-shit-done/bin/test/oc-profile-config.test.cjs +377 -0
  20. package/get-shit-done/bin/test/pivot-profile.test.cjs +276 -0
  21. package/get-shit-done/bin/test/set-profile.test.cjs +301 -0
  22. package/get-shit-done/workflows/oc-check-profile.md +181 -0
  23. package/get-shit-done/workflows/oc-set-profile.md +83 -243
  24. package/get-shit-done/workflows/settings.md +4 -3
  25. package/package.json +2 -2
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Unit tests for set-profile.cjs
3
+ *
4
+ * Tests for profile switching, validation, and the three operation modes:
5
+ * 1. Mode 1 (no profile name): Validate and apply current profile
6
+ * 2. Mode 2 (profile name): Switch to specified profile
7
+ * 3. Mode 3 (inline JSON): Create new profile from definition
8
+ *
9
+ * Includes validation checks, dry-run functionality, and rollback mechanisms.
10
+ */
11
+
12
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import os from 'os';
16
+
17
+ // Mock console.log and console.error to capture output
18
+ const originalLog = console.log;
19
+ const originalError = console.error;
20
+ const originalExit = process.exit;
21
+
22
+ // Test fixtures
23
+ const VALID_CONFIG = {
24
+ current_oc_profile: 'smart',
25
+ profiles: {
26
+ presets: {
27
+ simple: {
28
+ planning: 'bailian-coding-plan/qwen3.5-plus',
29
+ execution: 'bailian-coding-plan/qwen3.5-plus',
30
+ verification: 'bailian-coding-plan/qwen3.5-plus'
31
+ },
32
+ smart: {
33
+ planning: 'bailian-coding-plan/qwen3.5-plus',
34
+ execution: 'bailian-coding-plan/qwen3.5-plus',
35
+ verification: 'bailian-coding-plan/qwen3.5-plus'
36
+ },
37
+ genius: {
38
+ planning: 'bailian-coding-plan/qwen3.5-plus',
39
+ execution: 'bailian-coding-plan/qwen3.5-plus',
40
+ verification: 'bailian-coding-plan/qwen3.5-plus'
41
+ }
42
+ }
43
+ }
44
+ };
45
+
46
+ describe('set-profile.cjs', () => {
47
+ let testDir;
48
+ let planningDir;
49
+ let configPath;
50
+ let opencodePath;
51
+ let capturedLog;
52
+ let capturedError;
53
+ let exitCode;
54
+ let allLogs;
55
+ let allErrors;
56
+
57
+ beforeEach(() => {
58
+ // Create isolated test directory
59
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'set-profile-test-'));
60
+ planningDir = path.join(testDir, '.planning');
61
+ configPath = path.join(planningDir, 'oc_config.json');
62
+ opencodePath = path.join(testDir, 'opencode.json');
63
+
64
+ fs.mkdirSync(planningDir, { recursive: true });
65
+
66
+ // Reset captured output
67
+ capturedLog = null;
68
+ capturedError = null;
69
+ exitCode = null;
70
+ allLogs = [];
71
+ allErrors = [];
72
+
73
+ // Mock console.log to capture all output
74
+ console.log = (msg) => {
75
+ allLogs.push(msg);
76
+ capturedLog = msg;
77
+ };
78
+ console.error = (msg) => {
79
+ allErrors.push(msg);
80
+ capturedError = msg;
81
+ };
82
+ process.exit = (code) => {
83
+ exitCode = code;
84
+ throw new Error(`process.exit(${code})`);
85
+ };
86
+ });
87
+
88
+ afterEach(() => {
89
+ // Restore original functions
90
+ console.log = originalLog;
91
+ console.error = originalError;
92
+ process.exit = originalExit;
93
+
94
+ // Cleanup test directory
95
+ try {
96
+ fs.rmSync(testDir, { recursive: true, force: true });
97
+ } catch (err) {
98
+ // Ignore cleanup errors
99
+ }
100
+ });
101
+
102
+ // Import setProfile inside tests to use mocked functions
103
+ const importSetProfile = () => {
104
+ const modulePath = '../gsd-oc-commands/set-profile.cjs';
105
+ delete require.cache[require.resolve(modulePath)];
106
+ return require(modulePath);
107
+ };
108
+
109
+ describe('Export verification', () => {
110
+ it('exports setProfile function', () => {
111
+ const setProfile = importSetProfile();
112
+ expect(typeof setProfile).toBe('function');
113
+ });
114
+
115
+ it('function name is setProfile', () => {
116
+ const setProfile = importSetProfile();
117
+ expect(setProfile.name).toBe('setProfilePhase16'); // Function was renamed from phase16
118
+ });
119
+ });
120
+
121
+ describe('Basic functionality', () => {
122
+ function writeOpencodeJson() {
123
+ const opencode = {
124
+ $schema: 'https://opencode.ai/schema.json',
125
+ agent: {
126
+ 'gsd-planner': {
127
+ model: 'bailian-coding-plan/qwen3.5-plus',
128
+ tools: ['*']
129
+ },
130
+ 'gsd-executor': {
131
+ model: 'bailian-coding-plan/qwen3.5-plus',
132
+ tools: ['*']
133
+ }
134
+ }
135
+ };
136
+ fs.writeFileSync(opencodePath, JSON.stringify(opencode, null, 2) + '\n', 'utf8');
137
+ }
138
+
139
+ beforeEach(() => {
140
+ fs.writeFileSync(configPath, JSON.stringify(VALID_CONFIG, null, 2) + '\n', 'utf8');
141
+ writeOpencodeJson();
142
+ });
143
+
144
+ it('setProfile updates profile when profile name provided', () => {
145
+ const setProfile = importSetProfile();
146
+
147
+ try {
148
+ setProfile(testDir, ['genius']);
149
+ } catch (err) {
150
+ // Expected to throw due to process.exit mock
151
+ }
152
+
153
+ expect(exitCode).toBe(0);
154
+ const output = JSON.parse(capturedLog);
155
+ expect(output.success).toBe(true);
156
+ expect(output.data.profile).toBe('genius');
157
+ });
158
+
159
+ it('setProfile processes dry-run flag', () => {
160
+ const setProfile = importSetProfile();
161
+
162
+ try {
163
+ setProfile(testDir, ['smart', '--dry-run']);
164
+ } catch (err) {
165
+ // Expected
166
+ }
167
+
168
+ expect(exitCode).toBe(0);
169
+ const output = JSON.parse(capturedLog);
170
+ expect(output.success).toBe(true);
171
+ expect(output.data.dryRun).toBe(true);
172
+ expect(output.data.action).toBe('switch_profile');
173
+ });
174
+
175
+ it('setProfile validates required keys for inline profiles', () => {
176
+ const setProfile = importSetProfile();
177
+ const inlineProfile = 'test_profile:{"planning":"bailian-coding-plan/qwen3.5-plus","execution":"bailian-coding-plan/qwen3.5-plus","verification":"bailian-coding-plan/qwen3.5-plus"}';
178
+
179
+ try {
180
+ setProfile(testDir, [inlineProfile]);
181
+ } catch (err) {
182
+ // Expected
183
+ }
184
+
185
+ const output = JSON.parse(capturedLog);
186
+ expect(output.success).toBe(true);
187
+ expect(output.data.profile).toBe('test_profile');
188
+ });
189
+
190
+ it('setProfile handles Mode 1 (no profile name) scenario', () => {
191
+ const setProfile = importSetProfile();
192
+
193
+ try {
194
+ setProfile(testDir, []);
195
+ } catch (err) {
196
+ // Expected
197
+ }
198
+
199
+ expect(exitCode).toBe(0);
200
+ const output = JSON.parse(capturedLog);
201
+ expect(output.success).toBe(true);
202
+ expect(output.data.profile).toBe('smart'); // From initial current_oc_profile
203
+ });
204
+
205
+ it('setProfile validates invalid models before modification', () => {
206
+ const setProfile = importSetProfile();
207
+ const inlineProfile = 'bad_profile:{"planning":"bad_model","execution":"bad_model","verification":"bad_model"}';
208
+
209
+ try {
210
+ setProfile(testDir, [inlineProfile]);
211
+ } catch (err) {
212
+ // Expected - should error
213
+ }
214
+
215
+ expect(exitCode).toBe(1);
216
+ });
217
+
218
+ it('setProfile rejects invalid inline profile definitions', () => {
219
+ const setProfile = importSetProfile();
220
+ // Invalid JSON
221
+ const badDef = 'bad_profile:{"planning:"model","execution":"model","verification":"model"}';
222
+
223
+ try {
224
+ setProfile(testDir, [badDef]);
225
+ } catch (err) {
226
+ // Expected - should error
227
+ }
228
+
229
+ expect(exitCode).toBe(1);
230
+ const error = JSON.parse(capturedError);
231
+ expect(error.error.code).toBe('INVALID_SYNTAX');
232
+ });
233
+
234
+ it('setProfile rejects incomplete profile definitions', () => {
235
+ const setProfile = importSetProfile();
236
+ // Missing verification property
237
+ const badDef = 'bad_profile:{"planning":"bailian-coding-plan/qwen3.5-plus","execution":"bailian-coding-plan/qwen3.5-plus"}';
238
+
239
+ try {
240
+ setProfile(testDir, [badDef]);
241
+ } catch (err) {
242
+ // Expected - should error
243
+ }
244
+
245
+ expect(exitCode).toBe(1);
246
+ const error = JSON.parse(capturedError);
247
+ expect(error.error.code).toBe('INCOMPLETE_PROFILE');
248
+ });
249
+ });
250
+
251
+ describe('Error handling', () => {
252
+ it('handles missing config.json gracefully', () => {
253
+ const setProfile = importSetProfile();
254
+
255
+ try {
256
+ setProfile(testDir, ['test']);
257
+ } catch (err) {
258
+ // Expected to throw
259
+ }
260
+
261
+ expect(exitCode).toBe(1);
262
+ const error = JSON.parse(capturedError);
263
+ expect(error.error.code).toBe('CONFIG_NOT_FOUND');
264
+ });
265
+
266
+ it('sets exit code 1 for invalid profile', () => {
267
+ const setProfile = importSetProfile();
268
+
269
+ // Set up a valid config with presets
270
+ const configData = {...VALID_CONFIG};
271
+ fs.writeFileSync(configPath, JSON.stringify(configData, null, 2) + '\n', 'utf8');
272
+ const opencodeData = {
273
+ $schema: 'https://opencode.ai/schema.json',
274
+ agent: {}
275
+ };
276
+ fs.writeFileSync(opencodePath, JSON.stringify(opencodeData, null, 2) + '\n', 'utf8');
277
+
278
+ try {
279
+ setProfile(testDir, ['non-existent-profile']);
280
+ } catch (err) {
281
+ // Expected
282
+ }
283
+
284
+ expect(exitCode).toBe(1);
285
+ });
286
+
287
+ it('rejects too many arguments', () => {
288
+ const setProfile = importSetProfile();
289
+
290
+ try {
291
+ setProfile(testDir, ['profile1', 'profile2']);
292
+ } catch (err) {
293
+ // Expected
294
+ }
295
+
296
+ expect(exitCode).toBe(1);
297
+ const error = JSON.parse(capturedError);
298
+ expect(error.error.code).toBe('INVALID_ARGS');
299
+ });
300
+ });
301
+ });
@@ -0,0 +1,181 @@
1
+ <role>
2
+ You are executing the `/gsd-check-profile` command. Validate gsd-opencode profile configuration across both `opencode.json` and `.planning/oc_config.json`, then report results.
3
+
4
+ This is a **read-only diagnostic**. Do NOT modify any files or attempt to fix issues. When problems are found, recommend `/gsd-set-profile` and stop.
5
+ </role>
6
+
7
+ <required_reading>
8
+ read all files referenced by the invoking prompt's execution_context before starting.
9
+ </required_reading>
10
+
11
+ <context>
12
+ ## What Gets Validated
13
+
14
+ | Check | File | Validates |
15
+ |-------|------|-----------|
16
+ | `check-opencode-json` | `opencode.json` | All agent model IDs exist in the opencode models catalog |
17
+ | `check-config-json` | `.planning/oc_config.json` | gsd-opencode profile structure is valid, current profile exists in presets, all stage model IDs exist in catalog |
18
+
19
+ ## CLI Tool
20
+
21
+ All validation runs through `gsd-oc-tools.cjs`. Both commands output a JSON envelope with `success`, `data`, and optional `error` fields. Exit code 0 = valid, exit code 1 = issues found.
22
+
23
+ ## JSON Response Shapes
24
+
25
+ **check-opencode-json** (exit 0 or 1):
26
+ ```json
27
+ {
28
+ "success": true,
29
+ "data": { "valid": true|false, "total": N, "validCount": N, "invalidCount": N, "issues": [{ "agent": "...", "model": "...", "reason": "..." }] },
30
+ "error": { "code": "INVALID_MODEL_ID", "message": "..." }
31
+ }
32
+ ```
33
+
34
+ Note: When `opencode.json` does not exist, the tool returns exit 1 with `error.code = "CONFIG_NOT_FOUND"`. This is **not** an error for gsd-opencode profile validation — see Step 2 for handling.
35
+
36
+ **check-config-json** (exit 0 or 1):
37
+ ```json
38
+ {
39
+ "success": true|false,
40
+ "data": { "passed": true|false, "current_oc_profile": "...", "profile_data": {...}, "issues": [{ "field": "...", "value": "...", "reason": "..." }] },
41
+ "error": { "code": "INVALID_PROFILE|CONFIG_NOT_FOUND|INVALID_JSON", "message": "..." }
42
+ }
43
+ ```
44
+ </context>
45
+
46
+ <behavior>
47
+
48
+ ## Step 1: Run both validations
49
+
50
+ Execute both checks and capture their output and exit codes:
51
+
52
+ ```bash
53
+ node ~/.config/opencode/get-shit-done/bin/gsd-oc-tools.cjs check-opencode-json
54
+ ```
55
+
56
+ ```bash
57
+ node ~/.config/opencode/get-shit-done/bin/gsd-oc-tools.cjs check-config-json
58
+ ```
59
+
60
+ Parse both JSON responses.
61
+
62
+ ## Step 2: Classify results by severity
63
+
64
+ ### opencode.json classification
65
+
66
+ | Tool result | Severity | Meaning |
67
+ |-------------|----------|---------|
68
+ | exit 0, `data.valid = true` | OK | All model IDs valid |
69
+ | exit 1, `error.code = "CONFIG_NOT_FOUND"` | WARNING | No `opencode.json` — agents will use the default/current model. This is acceptable. |
70
+ | exit 1, `error.code = "INVALID_MODEL_ID"` | ERROR | One or more model IDs are invalid. Must be fixed. |
71
+ | exit 1, `error.code = "INVALID_JSON"` | ERROR | File is malformed JSON. Must be fixed. |
72
+
73
+ ### .planning/oc_config.json classification
74
+
75
+ | Tool result | Severity | Meaning |
76
+ |-------------|----------|---------|
77
+ | exit 0, `data.passed = true` | OK | gsd-opencode profile configuration valid |
78
+ | exit 1, `error.code = "CONFIG_NOT_FOUND"` | ERROR | No gsd-opencode profile configured yet |
79
+ | exit 1, `error.code = "INVALID_PROFILE"` | ERROR | gsd-opencode profile structure is invalid |
80
+ | exit 1, `error.code = "INVALID_JSON"` | ERROR | File is malformed JSON |
81
+
82
+ ## Step 3: Report results
83
+
84
+ Determine the overall status:
85
+ - **All OK (no ERRORs, no WARNINGs)**: report success
86
+ - **WARNINGs only (no ERRORs)**: report success with warnings
87
+ - **Any ERRORs**: report errors with fix instructions
88
+
89
+ ---
90
+
91
+ ### All OK — no errors, no warnings
92
+
93
+ ```
94
+ gsd-opencode profile: OK
95
+
96
+ opencode.json All model IDs valid
97
+ .planning/oc_config.json gsd-opencode profile valid
98
+ ```
99
+
100
+ **Stop here.**
101
+
102
+ ---
103
+
104
+ ### OK with warnings (opencode.json missing, but oc_config.json is valid)
105
+
106
+ ```
107
+ gsd-opencode profile: OK
108
+
109
+ opencode.json Not found (agents will use the default/current model)
110
+ .planning/oc_config.json gsd-opencode profile valid
111
+ ```
112
+
113
+ **Stop here.**
114
+
115
+ ---
116
+
117
+ ### Errors found
118
+
119
+ Display a structured diagnostic. Use the severity labels (WARNING / ERROR) to make the impact clear.
120
+
121
+ ```
122
+ gsd-opencode profile: ERRORS FOUND
123
+
124
+ --- opencode.json ---
125
+
126
+ [If OK]
127
+ All model IDs valid
128
+
129
+ [If WARNING — CONFIG_NOT_FOUND]
130
+ WARNING: opencode.json not found. Agents will use the default/current model.
131
+
132
+ [If ERROR — INVALID_MODEL_ID — iterate over data.issues]
133
+ ERROR: {N} invalid model ID(s):
134
+
135
+ Agent: {issue.agent}
136
+ Model: {issue.model}
137
+ Reason: {issue.reason}
138
+
139
+ (repeat for each issue)
140
+
141
+ [If ERROR — INVALID_JSON]
142
+ ERROR: opencode.json is not valid JSON.
143
+
144
+ --- .planning/oc_config.json ---
145
+
146
+ [If OK]
147
+ gsd-opencode profile valid
148
+
149
+ [If ERROR — CONFIG_NOT_FOUND]
150
+ ERROR: .planning/oc_config.json not found — no gsd-opencode profile configured.
151
+
152
+ [If ERROR — INVALID_PROFILE — iterate over data.issues]
153
+ ERROR: {N} gsd-opencode profile issue(s):
154
+
155
+ Field: {issue.field}
156
+ Value: {issue.value}
157
+ Reason: {issue.reason}
158
+
159
+ (repeat for each issue)
160
+
161
+ [If ERROR — INVALID_JSON]
162
+ ERROR: .planning/oc_config.json is not valid JSON.
163
+
164
+ --- Fix ---
165
+
166
+ Run /gsd-set-profile or /gsd-set-profile <simple|smart|genius> to fix gsd-opencode profile configuration.
167
+ ```
168
+
169
+ **Stop here.** Do not offer to fix anything. Do not edit files.
170
+
171
+ </behavior>
172
+
173
+ <notes>
174
+ - This workflow is strictly diagnostic — never modify `opencode.json`, `.planning/oc_config.json`, or any other file.
175
+ - When errors are found, always recommend `/gsd-set-profile` or `/gsd-set-profile <simple|smart|genius>` as the resolution path. Do not suggest manual editing.
176
+ - Always display full model IDs (e.g., `bailian-coding-plan/qwen3-coder-plus`), never abbreviate.
177
+ - Missing `opencode.json` is a WARNING, not an error. The user simply hasn't customized agent models — agents fall back to the default/current model. Do not include it in the "Fix" section.
178
+ - Missing `.planning/oc_config.json` IS an error — it means no gsd-opencode profile has been set up.
179
+ - Always use the term "gsd-opencode profile" (not just "profile") when referring to the profile system.
180
+ - Both `check-config-json` and `check-oc-config-json` route to the same validator. Use `check-config-json` (shorter).
181
+ </notes>