@ktpartners/dgs-platform 2.6.3 → 2.7.1
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/agents/dgs-executor.md +51 -0
- package/commands/dgs/sync.md +70 -0
- package/deliver-great-systems/bin/dgs-tools.cjs +290 -4
- package/deliver-great-systems/bin/lib/config.cjs +259 -67
- package/deliver-great-systems/bin/lib/core.cjs +49 -8
- package/deliver-great-systems/bin/lib/core.test.cjs +35 -14
- package/deliver-great-systems/bin/lib/init.cjs +61 -6
- package/deliver-great-systems/bin/lib/init.test.cjs +9 -9
- package/deliver-great-systems/bin/lib/migration.cjs +1 -1
- package/deliver-great-systems/bin/lib/migration.test.cjs +7 -9
- package/deliver-great-systems/bin/lib/path-audit.test.cjs +1 -1
- package/deliver-great-systems/bin/lib/paths.cjs +32 -22
- package/deliver-great-systems/bin/lib/paths.test.cjs +16 -6
- package/deliver-great-systems/bin/lib/projects.cjs +1 -1
- package/deliver-great-systems/bin/lib/projects.test.cjs +1 -1
- package/deliver-great-systems/bin/lib/repos.cjs +29 -10
- package/deliver-great-systems/bin/lib/state.cjs +2 -2
- package/deliver-great-systems/bin/lib/sync.cjs +878 -0
- package/deliver-great-systems/bin/lib/test-helpers.cjs +44 -12
- package/deliver-great-systems/references/git-integration.md +81 -0
- package/deliver-great-systems/references/planning-config.md +154 -31
- package/deliver-great-systems/references/sync-cadence.md +191 -0
- package/deliver-great-systems/references/sync-hooks.md +96 -0
- package/deliver-great-systems/test/cadence.test.cjs +160 -0
- package/deliver-great-systems/test/sync-workflow.test.cjs +562 -0
- package/deliver-great-systems/workflows/execute-phase.md +111 -4
- package/deliver-great-systems/workflows/init-product.md +6 -2
- package/deliver-great-systems/workflows/run-job.md +77 -2
- package/deliver-great-systems/workflows/settings.md +82 -1
- package/package.json +1 -1
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Config
|
|
2
|
+
* Config -- Planning config CRUD operations
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
4
|
+
* Two-file config layout:
|
|
5
|
+
* - config.json (tracked): shared team settings (mode, workflows, git, model_profile, etc.)
|
|
6
|
+
* - config.local.json (gitignored): per-machine state (current_project, planningRoot, hint flags)
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* LOCAL_KEYS defines which keys route to config.local.json.
|
|
9
|
+
* writeConfigField routes writes to the correct file based on isLocalKey().
|
|
10
|
+
* loadConfig (in core.cjs) merges both files with local overriding shared.
|
|
11
|
+
*
|
|
12
|
+
* Zero hardcoded .planning/config.json paths -- all resolved via getPlanningRoot.
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
15
|
const fs = require('fs');
|
|
@@ -13,6 +17,17 @@ const path = require('path');
|
|
|
13
17
|
const { output, error } = require('./core.cjs');
|
|
14
18
|
const { getPlanningRoot } = require('./paths.cjs');
|
|
15
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Keys that belong in config.local.json (per-machine state).
|
|
22
|
+
* These are stored as top-level keys in config.local.json.
|
|
23
|
+
*/
|
|
24
|
+
const LOCAL_KEYS = new Set([
|
|
25
|
+
'current_project',
|
|
26
|
+
'planningRoot',
|
|
27
|
+
'v2_hint_shown',
|
|
28
|
+
'sync_hint_shown',
|
|
29
|
+
]);
|
|
30
|
+
|
|
16
31
|
const VALID_CONFIG_KEYS = new Set([
|
|
17
32
|
'mode', 'granularity', 'parallelization', 'commit_docs', 'model_profile',
|
|
18
33
|
'search_gitignored', 'brave_search',
|
|
@@ -20,31 +35,144 @@ const VALID_CONFIG_KEYS = new Set([
|
|
|
20
35
|
'workflow.nyquist_validation', 'workflow.ui_phase', 'workflow.ui_safety_gate',
|
|
21
36
|
'workflow._auto_chain_active', 'workflow.discipline',
|
|
22
37
|
'git.base_branch', 'git.branching_strategy', 'git.phase_branch_template', 'git.milestone_branch_template',
|
|
38
|
+
'git.sync', 'git.sync_push', 'git.sync_pull',
|
|
23
39
|
'planning.commit_docs', 'planning.search_gitignored',
|
|
24
40
|
]);
|
|
25
41
|
|
|
26
42
|
/**
|
|
43
|
+
* Get the path to the shared (tracked) config file.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} cwd - Working directory
|
|
46
|
+
* @returns {string} Absolute path to config.json
|
|
47
|
+
*/
|
|
48
|
+
function getSharedConfigPath(cwd) {
|
|
49
|
+
return path.join(getPlanningRoot(cwd), 'config.json');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get the path to the local (gitignored) config file.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} cwd - Working directory
|
|
56
|
+
* @returns {string} Absolute path to config.local.json
|
|
57
|
+
*/
|
|
58
|
+
function getLocalConfigPath(cwd) {
|
|
59
|
+
return path.join(getPlanningRoot(cwd), 'config.local.json');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @deprecated Use getSharedConfigPath or getLocalConfigPath instead.
|
|
27
64
|
* Resolve the config file path for the given cwd and mode.
|
|
65
|
+
* Now returns config.json (shared) for both read and write modes.
|
|
66
|
+
* Falls back to legacy dgs.config.json for read mode.
|
|
28
67
|
*
|
|
29
68
|
* @param {string} cwd - Working directory
|
|
30
|
-
* @param {'read'|'write'} mode - 'read'
|
|
31
|
-
* 'write' for always dgs.config.json
|
|
69
|
+
* @param {'read'|'write'} mode - 'read' or 'write'
|
|
32
70
|
* @returns {string} Absolute path to the config file
|
|
33
71
|
*/
|
|
34
72
|
function getConfigPath(cwd, mode) {
|
|
35
73
|
const root = getPlanningRoot(cwd);
|
|
36
|
-
const
|
|
37
|
-
if (mode === 'write') return
|
|
38
|
-
// Read mode: prefer
|
|
39
|
-
if (fs.existsSync(
|
|
40
|
-
const legacyPath = path.join(root, 'config.json');
|
|
74
|
+
const sharedPath = path.join(root, 'config.json');
|
|
75
|
+
if (mode === 'write') return sharedPath;
|
|
76
|
+
// Read mode: prefer config.json, fall back to legacy dgs.config.json
|
|
77
|
+
if (fs.existsSync(sharedPath)) return sharedPath;
|
|
78
|
+
const legacyPath = path.join(root, 'dgs.config.json');
|
|
41
79
|
if (fs.existsSync(legacyPath)) return legacyPath;
|
|
42
|
-
return
|
|
80
|
+
return sharedPath; // default when neither exists
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if a key path should be stored in config.local.json.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} keyPath - Dot-notation key (e.g., 'current_project', 'workflow.research')
|
|
87
|
+
* @returns {boolean} True if the key belongs in config.local.json
|
|
88
|
+
*/
|
|
89
|
+
function isLocalKey(keyPath) {
|
|
90
|
+
// Check the key itself or its top-level segment
|
|
91
|
+
if (LOCAL_KEYS.has(keyPath)) return true;
|
|
92
|
+
const topLevel = keyPath.split('.')[0];
|
|
93
|
+
return LOCAL_KEYS.has(topLevel);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Read a JSON file safely.
|
|
98
|
+
* @param {string} filePath
|
|
99
|
+
* @returns {object}
|
|
100
|
+
*/
|
|
101
|
+
function _readJsonSafe(filePath) {
|
|
102
|
+
try {
|
|
103
|
+
if (fs.existsSync(filePath)) {
|
|
104
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
105
|
+
}
|
|
106
|
+
} catch { /* ignore */ }
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Write a JSON object to a file, creating parent directories as needed.
|
|
112
|
+
* @param {string} filePath
|
|
113
|
+
* @param {object} data
|
|
114
|
+
*/
|
|
115
|
+
function _writeJson(filePath, data) {
|
|
116
|
+
const dir = path.dirname(filePath);
|
|
117
|
+
if (!fs.existsSync(dir)) {
|
|
118
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
119
|
+
}
|
|
120
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Perform a one-time migration of legacy dgs.config.json to the two-file layout.
|
|
125
|
+
* Separates local keys from shared keys and writes to the appropriate files.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} cwd - Working directory
|
|
128
|
+
* @returns {boolean} True if migration was performed
|
|
129
|
+
*/
|
|
130
|
+
function _migrateLegacyConfig(cwd) {
|
|
131
|
+
const root = getPlanningRoot(cwd);
|
|
132
|
+
const legacyPath = path.join(root, 'dgs.config.json');
|
|
133
|
+
const sharedPath = path.join(root, 'config.json');
|
|
134
|
+
const localPath = path.join(root, 'config.local.json');
|
|
135
|
+
|
|
136
|
+
if (!fs.existsSync(legacyPath)) return false;
|
|
137
|
+
if (fs.existsSync(sharedPath) || fs.existsSync(localPath)) return false;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const legacy = JSON.parse(fs.readFileSync(legacyPath, 'utf-8'));
|
|
141
|
+
const shared = {};
|
|
142
|
+
const local = {};
|
|
143
|
+
|
|
144
|
+
for (const [key, value] of Object.entries(legacy)) {
|
|
145
|
+
if (LOCAL_KEYS.has(key)) {
|
|
146
|
+
local[key] = value;
|
|
147
|
+
} else if (key === 'git' && typeof value === 'object') {
|
|
148
|
+
// Split git section: sync_hint_shown is local, rest is shared
|
|
149
|
+
const gitShared = {};
|
|
150
|
+
for (const [gk, gv] of Object.entries(value)) {
|
|
151
|
+
if (gk === 'sync_hint_shown') {
|
|
152
|
+
local.sync_hint_shown = gv;
|
|
153
|
+
} else {
|
|
154
|
+
gitShared[gk] = gv;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (Object.keys(gitShared).length > 0) {
|
|
158
|
+
shared.git = gitShared;
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
shared[key] = value;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (Object.keys(shared).length > 0) _writeJson(sharedPath, shared);
|
|
166
|
+
if (Object.keys(local).length > 0) _writeJson(localPath, local);
|
|
167
|
+
return true;
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
43
171
|
}
|
|
44
172
|
|
|
45
173
|
function cmdConfigEnsureSection(cwd, raw) {
|
|
46
|
-
const
|
|
47
|
-
const parentDir = path.dirname(
|
|
174
|
+
const sharedPath = getSharedConfigPath(cwd);
|
|
175
|
+
const parentDir = path.dirname(sharedPath);
|
|
48
176
|
|
|
49
177
|
// Ensure parent directory exists
|
|
50
178
|
try {
|
|
@@ -55,9 +183,18 @@ function cmdConfigEnsureSection(cwd, raw) {
|
|
|
55
183
|
error('Failed to create config directory: ' + err.message);
|
|
56
184
|
}
|
|
57
185
|
|
|
58
|
-
// Check if config already exists (
|
|
59
|
-
|
|
60
|
-
|
|
186
|
+
// Check if config already exists (shared config.json or legacy dgs.config.json)
|
|
187
|
+
if (fs.existsSync(sharedPath)) {
|
|
188
|
+
const result = { created: false, reason: 'already_exists' };
|
|
189
|
+
output(result, raw, 'exists');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check for legacy dgs.config.json and migrate if found
|
|
194
|
+
const root = getPlanningRoot(cwd);
|
|
195
|
+
const legacyPath = path.join(root, 'dgs.config.json');
|
|
196
|
+
if (fs.existsSync(legacyPath)) {
|
|
197
|
+
_migrateLegacyConfig(cwd);
|
|
61
198
|
const result = { created: false, reason: 'already_exists' };
|
|
62
199
|
output(result, raw, 'exists');
|
|
63
200
|
return;
|
|
@@ -87,6 +224,7 @@ function cmdConfigEnsureSection(cwd, raw) {
|
|
|
87
224
|
}
|
|
88
225
|
|
|
89
226
|
// Create default config (user-level defaults override hardcoded defaults)
|
|
227
|
+
// Only shared settings go to config.json -- no local keys
|
|
90
228
|
const hardcoded = {
|
|
91
229
|
model_profile: 'balanced',
|
|
92
230
|
commit_docs: true,
|
|
@@ -102,6 +240,10 @@ function cmdConfigEnsureSection(cwd, raw) {
|
|
|
102
240
|
nyquist_validation: true,
|
|
103
241
|
discipline: true,
|
|
104
242
|
},
|
|
243
|
+
git: {
|
|
244
|
+
sync_push: 'prompt',
|
|
245
|
+
sync_pull: 'prompt',
|
|
246
|
+
},
|
|
105
247
|
parallelization: true,
|
|
106
248
|
brave_search: hasBraveSearch,
|
|
107
249
|
};
|
|
@@ -109,14 +251,20 @@ function cmdConfigEnsureSection(cwd, raw) {
|
|
|
109
251
|
...hardcoded,
|
|
110
252
|
...userDefaults,
|
|
111
253
|
workflow: { ...hardcoded.workflow, ...(userDefaults.workflow || {}) },
|
|
254
|
+
git: { ...hardcoded.git, ...(userDefaults.git || {}) },
|
|
112
255
|
};
|
|
113
256
|
|
|
257
|
+
// Remove any local keys that might have leaked in from userDefaults
|
|
258
|
+
for (const lk of LOCAL_KEYS) {
|
|
259
|
+
delete defaults[lk];
|
|
260
|
+
}
|
|
261
|
+
|
|
114
262
|
try {
|
|
115
|
-
fs.writeFileSync(
|
|
116
|
-
const result = { created: true, path: path.relative(cwd,
|
|
263
|
+
fs.writeFileSync(sharedPath, JSON.stringify(defaults, null, 2), 'utf-8');
|
|
264
|
+
const result = { created: true, path: path.relative(cwd, sharedPath) || sharedPath };
|
|
117
265
|
output(result, raw, 'created');
|
|
118
266
|
} catch (err) {
|
|
119
|
-
error('Failed to create
|
|
267
|
+
error('Failed to create config.json: ' + err.message);
|
|
120
268
|
}
|
|
121
269
|
}
|
|
122
270
|
|
|
@@ -129,21 +277,38 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
129
277
|
error(`Unknown config key: "${keyPath}". Valid keys: ${[...VALID_CONFIG_KEYS].sort().join(', ')}`);
|
|
130
278
|
}
|
|
131
279
|
|
|
280
|
+
// Validate sync mode values
|
|
281
|
+
const VALID_SYNC_MODES = new Set(['off', 'prompt', 'auto']);
|
|
282
|
+
if ((keyPath === 'git.sync_push' || keyPath === 'git.sync_pull' || keyPath === 'git.sync') && !VALID_SYNC_MODES.has(value)) {
|
|
283
|
+
error(`Invalid sync mode: "${value}". Valid values: off, prompt, auto`);
|
|
284
|
+
}
|
|
285
|
+
|
|
132
286
|
// Parse value (handle booleans and numbers)
|
|
133
287
|
let parsedValue = value;
|
|
134
288
|
if (value === 'true') parsedValue = true;
|
|
135
289
|
else if (value === 'false') parsedValue = false;
|
|
136
290
|
else if (!isNaN(value) && value !== '') parsedValue = Number(value);
|
|
137
291
|
|
|
138
|
-
// Load existing
|
|
139
|
-
const
|
|
140
|
-
let config =
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
292
|
+
// Load existing shared config
|
|
293
|
+
const sharedPath = getSharedConfigPath(cwd);
|
|
294
|
+
let config = _readJsonSafe(sharedPath);
|
|
295
|
+
|
|
296
|
+
// git.sync is a virtual shorthand -- sets both sync_push and sync_pull in shared config
|
|
297
|
+
if (keyPath === 'git.sync') {
|
|
298
|
+
if (!config.git || typeof config.git !== 'object') {
|
|
299
|
+
config.git = {};
|
|
144
300
|
}
|
|
145
|
-
|
|
146
|
-
|
|
301
|
+
config.git.sync_push = parsedValue;
|
|
302
|
+
config.git.sync_pull = parsedValue;
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
_writeJson(sharedPath, config);
|
|
306
|
+
const result = { updated: true, key: keyPath, value: parsedValue, expanded: ['git.sync_push', 'git.sync_pull'] };
|
|
307
|
+
output(result, raw, `git.sync_push=${parsedValue}, git.sync_pull=${parsedValue}`);
|
|
308
|
+
} catch (err) {
|
|
309
|
+
error('Failed to write config.json: ' + err.message);
|
|
310
|
+
}
|
|
311
|
+
return; // output() calls process.exit, but guard with return
|
|
147
312
|
}
|
|
148
313
|
|
|
149
314
|
// Set nested value using dot notation (e.g., "workflow.research")
|
|
@@ -158,14 +323,13 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
158
323
|
}
|
|
159
324
|
current[keys[keys.length - 1]] = parsedValue;
|
|
160
325
|
|
|
161
|
-
// Write to
|
|
162
|
-
const writePath = getConfigPath(cwd, 'write');
|
|
326
|
+
// Write to shared config.json (all VALID_CONFIG_KEYS are shared)
|
|
163
327
|
try {
|
|
164
|
-
|
|
328
|
+
_writeJson(sharedPath, config);
|
|
165
329
|
const result = { updated: true, key: keyPath, value: parsedValue };
|
|
166
330
|
output(result, raw, `${keyPath}=${parsedValue}`);
|
|
167
331
|
} catch (err) {
|
|
168
|
-
error('Failed to write
|
|
332
|
+
error('Failed to write config.json: ' + err.message);
|
|
169
333
|
}
|
|
170
334
|
}
|
|
171
335
|
|
|
@@ -174,17 +338,21 @@ function cmdConfigGet(cwd, keyPath, raw) {
|
|
|
174
338
|
error('Usage: config-get <key.path>');
|
|
175
339
|
}
|
|
176
340
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
341
|
+
// Read both files and merge (local overrides shared)
|
|
342
|
+
const shared = _readJsonSafe(getSharedConfigPath(cwd));
|
|
343
|
+
const local = _readJsonSafe(getLocalConfigPath(cwd));
|
|
344
|
+
|
|
345
|
+
// Fall back to legacy dgs.config.json if neither new file has data
|
|
346
|
+
let config;
|
|
347
|
+
if (Object.keys(shared).length === 0 && Object.keys(local).length === 0) {
|
|
348
|
+
const root = getPlanningRoot(cwd);
|
|
349
|
+
const legacyPath = path.join(root, 'dgs.config.json');
|
|
350
|
+
config = _readJsonSafe(legacyPath);
|
|
351
|
+
if (Object.keys(config).length === 0) {
|
|
352
|
+
error('No config file found at ' + getSharedConfigPath(cwd));
|
|
184
353
|
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
error('Failed to read config: ' + err.message);
|
|
354
|
+
} else {
|
|
355
|
+
config = { ...shared, ...local };
|
|
188
356
|
}
|
|
189
357
|
|
|
190
358
|
// Traverse dot-notation path (e.g., "workflow.auto_advance")
|
|
@@ -206,28 +374,46 @@ function cmdConfigGet(cwd, keyPath, raw) {
|
|
|
206
374
|
|
|
207
375
|
/**
|
|
208
376
|
* Programmatically set a config field. Uses same dot-notation as cmdConfigSet.
|
|
209
|
-
* Does NOT call output()/process.exit()
|
|
377
|
+
* Does NOT call output()/process.exit() -- returns the updated merged config.
|
|
210
378
|
*
|
|
211
|
-
*
|
|
379
|
+
* Routes writes to the correct file based on isLocalKey():
|
|
380
|
+
* - Local keys (current_project, planningRoot, v2_hint_shown, sync_hint_shown) -> config.local.json
|
|
381
|
+
* - Shared keys (everything else) -> config.json
|
|
212
382
|
*
|
|
213
383
|
* @param {string} cwd - Working directory
|
|
214
384
|
* @param {string} keyPath - Dot-notation key (e.g., "product_name" or "workflow.research")
|
|
215
385
|
* @param {*} value - Value to set
|
|
216
|
-
* @returns {object} Updated config object
|
|
386
|
+
* @returns {object} Updated merged config object
|
|
217
387
|
*/
|
|
218
388
|
function writeConfigField(cwd, keyPath, value) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
389
|
+
// Attempt migration if needed
|
|
390
|
+
const sharedPath = getSharedConfigPath(cwd);
|
|
391
|
+
const localPath = getLocalConfigPath(cwd);
|
|
392
|
+
if (!fs.existsSync(sharedPath) && !fs.existsSync(localPath)) {
|
|
393
|
+
_migrateLegacyConfig(cwd);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// git.sync is a virtual shorthand -- sets both sync_push and sync_pull in shared config
|
|
397
|
+
if (keyPath === 'git.sync') {
|
|
398
|
+
let config = _readJsonSafe(sharedPath);
|
|
399
|
+
if (!config.git || typeof config.git !== 'object') {
|
|
400
|
+
config.git = {};
|
|
224
401
|
}
|
|
225
|
-
|
|
226
|
-
|
|
402
|
+
config.git.sync_push = value;
|
|
403
|
+
config.git.sync_pull = value;
|
|
404
|
+
_writeJson(sharedPath, config);
|
|
405
|
+
// Return merged config
|
|
406
|
+
const local = _readJsonSafe(localPath);
|
|
407
|
+
return { ...config, ...local };
|
|
227
408
|
}
|
|
228
409
|
|
|
410
|
+
// Determine target file
|
|
411
|
+
const targetPath = isLocalKey(keyPath) ? localPath : sharedPath;
|
|
412
|
+
let targetConfig = _readJsonSafe(targetPath);
|
|
413
|
+
|
|
414
|
+
// Set the value using dot-notation
|
|
229
415
|
const keys = keyPath.split('.');
|
|
230
|
-
let current =
|
|
416
|
+
let current = targetConfig;
|
|
231
417
|
for (let i = 0; i < keys.length - 1; i++) {
|
|
232
418
|
const key = keys[i];
|
|
233
419
|
if (current[key] === undefined || typeof current[key] !== 'object') {
|
|
@@ -237,9 +423,12 @@ function writeConfigField(cwd, keyPath, value) {
|
|
|
237
423
|
}
|
|
238
424
|
current[keys[keys.length - 1]] = value;
|
|
239
425
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
426
|
+
_writeJson(targetPath, targetConfig);
|
|
427
|
+
|
|
428
|
+
// Return merged config for backward compat
|
|
429
|
+
const shared = _readJsonSafe(sharedPath);
|
|
430
|
+
const local = _readJsonSafe(localPath);
|
|
431
|
+
return { ...shared, ...local };
|
|
243
432
|
}
|
|
244
433
|
|
|
245
434
|
/**
|
|
@@ -360,8 +549,8 @@ function checkConfigGitignore(cwd) {
|
|
|
360
549
|
}
|
|
361
550
|
|
|
362
551
|
/**
|
|
363
|
-
* Ensure config
|
|
364
|
-
*
|
|
552
|
+
* Ensure config.local.json and review-keys.json are in .gitignore.
|
|
553
|
+
* config.json is TRACKED (shared settings), so it is NOT added to .gitignore.
|
|
365
554
|
*
|
|
366
555
|
* @param {string} cwd - Working directory
|
|
367
556
|
* @returns {object} { added: boolean, reason?: string }
|
|
@@ -378,22 +567,21 @@ function ensureConfigGitignored(cwd) {
|
|
|
378
567
|
// If unreadable, start fresh
|
|
379
568
|
}
|
|
380
569
|
|
|
381
|
-
// Check if already present
|
|
570
|
+
// Check if already present
|
|
382
571
|
const lines = content.split('\n').map((l) => l.trim());
|
|
383
|
-
const
|
|
384
|
-
const hasNew = lines.some((line) => line === '.planning/dgs.config.json' || line === 'dgs.config.json');
|
|
572
|
+
const hasLocalConfig = lines.some((line) => line === 'config.local.json' || line === '.planning/config.local.json');
|
|
385
573
|
const hasReviewKeys = lines.some((line) => line === 'review-keys.json' || line === '.planning/review-keys.json');
|
|
386
574
|
|
|
387
|
-
if (
|
|
575
|
+
if (hasLocalConfig && hasReviewKeys) {
|
|
388
576
|
return { added: false, reason: 'already_present' };
|
|
389
577
|
}
|
|
390
578
|
|
|
391
579
|
// Append missing entries
|
|
392
580
|
let entry = '';
|
|
393
|
-
if (!
|
|
394
|
-
entry += '\n# DGS config (
|
|
395
|
-
|
|
396
|
-
|
|
581
|
+
if (!hasLocalConfig) {
|
|
582
|
+
entry += '\n# DGS local config (per-machine state)\n';
|
|
583
|
+
entry += 'config.local.json\n';
|
|
584
|
+
entry += '.planning/config.local.json\n';
|
|
397
585
|
}
|
|
398
586
|
if (!hasReviewKeys) {
|
|
399
587
|
entry += '\n# DGS review keys (contains API keys)\n';
|
|
@@ -405,7 +593,11 @@ function ensureConfigGitignored(cwd) {
|
|
|
405
593
|
}
|
|
406
594
|
|
|
407
595
|
module.exports = {
|
|
596
|
+
LOCAL_KEYS,
|
|
408
597
|
getConfigPath,
|
|
598
|
+
getSharedConfigPath,
|
|
599
|
+
getLocalConfigPath,
|
|
600
|
+
isLocalKey,
|
|
409
601
|
getReviewKeysPath,
|
|
410
602
|
cmdConfigEnsureSection,
|
|
411
603
|
cmdConfigSet,
|
|
@@ -67,9 +67,6 @@ function safeReadFile(filePath) {
|
|
|
67
67
|
|
|
68
68
|
function loadConfig(cwd) {
|
|
69
69
|
const root = getPlanningRoot(cwd);
|
|
70
|
-
const newConfigPath = path.join(root, 'dgs.config.json');
|
|
71
|
-
const legacyConfigPath = path.join(root, 'config.json');
|
|
72
|
-
const configPath = fs.existsSync(newConfigPath) ? newConfigPath : legacyConfigPath;
|
|
73
70
|
const defaults = {
|
|
74
71
|
model_profile: 'balanced',
|
|
75
72
|
commit_docs: true,
|
|
@@ -78,6 +75,8 @@ function loadConfig(cwd) {
|
|
|
78
75
|
phase_branch_template: 'dgs/{project}/phase-{phase}-{slug}',
|
|
79
76
|
milestone_branch_template: 'dgs/{project}/{milestone}-{slug}',
|
|
80
77
|
base_branch: 'main',
|
|
78
|
+
sync_push: 'off',
|
|
79
|
+
sync_pull: 'off',
|
|
81
80
|
research: true,
|
|
82
81
|
plan_checker: true,
|
|
83
82
|
verifier: true,
|
|
@@ -85,16 +84,55 @@ function loadConfig(cwd) {
|
|
|
85
84
|
brave_search: false,
|
|
86
85
|
};
|
|
87
86
|
|
|
87
|
+
// Read shared config (config.json)
|
|
88
|
+
let shared = {};
|
|
89
|
+
const sharedPath = path.join(root, 'config.json');
|
|
88
90
|
try {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
if (fs.existsSync(sharedPath)) {
|
|
92
|
+
shared = JSON.parse(fs.readFileSync(sharedPath, 'utf-8'));
|
|
93
|
+
}
|
|
94
|
+
} catch { /* ignore */ }
|
|
95
|
+
|
|
96
|
+
// Read local config (config.local.json)
|
|
97
|
+
let local = {};
|
|
98
|
+
const localPath = path.join(root, 'config.local.json');
|
|
99
|
+
try {
|
|
100
|
+
if (fs.existsSync(localPath)) {
|
|
101
|
+
local = JSON.parse(fs.readFileSync(localPath, 'utf-8'));
|
|
102
|
+
}
|
|
103
|
+
} catch { /* ignore */ }
|
|
91
104
|
|
|
105
|
+
// Fall back to legacy dgs.config.json if neither new file exists
|
|
106
|
+
let hasNewConfig = Object.keys(shared).length > 0 || Object.keys(local).length > 0;
|
|
107
|
+
if (!hasNewConfig) {
|
|
108
|
+
const legacyPath = path.join(root, 'dgs.config.json');
|
|
109
|
+
try {
|
|
110
|
+
if (fs.existsSync(legacyPath)) {
|
|
111
|
+
shared = JSON.parse(fs.readFileSync(legacyPath, 'utf-8'));
|
|
112
|
+
hasNewConfig = true;
|
|
113
|
+
}
|
|
114
|
+
} catch { /* ignore */ }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!hasNewConfig) {
|
|
118
|
+
return { ...defaults, current_project: null, v2_hint_shown: false, sync_hint_shown: false };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Merge: local overrides shared for overlapping keys
|
|
122
|
+
const parsed = { ...shared, ...local };
|
|
123
|
+
|
|
124
|
+
try {
|
|
92
125
|
// Migrate deprecated "depth" key to "granularity" with value mapping
|
|
93
126
|
if ('depth' in parsed && !('granularity' in parsed)) {
|
|
94
127
|
const depthToGranularity = { quick: 'coarse', standard: 'standard', comprehensive: 'fine' };
|
|
95
128
|
parsed.granularity = depthToGranularity[parsed.depth] || parsed.depth;
|
|
96
129
|
delete parsed.depth;
|
|
97
|
-
|
|
130
|
+
// Write back to shared config if that's where depth was
|
|
131
|
+
if ('depth' in shared) {
|
|
132
|
+
shared.granularity = parsed.granularity;
|
|
133
|
+
delete shared.depth;
|
|
134
|
+
try { fs.writeFileSync(sharedPath, JSON.stringify(shared, null, 2), 'utf-8'); } catch {}
|
|
135
|
+
}
|
|
98
136
|
}
|
|
99
137
|
|
|
100
138
|
const get = (key, nested) => {
|
|
@@ -120,18 +158,21 @@ function loadConfig(cwd) {
|
|
|
120
158
|
phase_branch_template: get('phase_branch_template', { section: 'git', field: 'phase_branch_template' }) ?? defaults.phase_branch_template,
|
|
121
159
|
milestone_branch_template: get('milestone_branch_template', { section: 'git', field: 'milestone_branch_template' }) ?? defaults.milestone_branch_template,
|
|
122
160
|
base_branch: get('base_branch', { section: 'git', field: 'base_branch' }) ?? defaults.base_branch,
|
|
161
|
+
sync_push: get('sync_push', { section: 'git', field: 'sync_push' }) ?? defaults.sync_push,
|
|
162
|
+
sync_pull: get('sync_pull', { section: 'git', field: 'sync_pull' }) ?? defaults.sync_pull,
|
|
163
|
+
sync_hint_shown: get('sync_hint_shown', { section: 'git', field: 'sync_hint_shown' }) ?? false,
|
|
123
164
|
research: get('research', { section: 'workflow', field: 'research' }) ?? defaults.research,
|
|
124
165
|
plan_checker: get('plan_checker', { section: 'workflow', field: 'plan_check' }) ?? defaults.plan_checker,
|
|
125
166
|
verifier: get('verifier', { section: 'workflow', field: 'verifier' }) ?? defaults.verifier,
|
|
126
167
|
parallelization,
|
|
127
168
|
brave_search: get('brave_search') ?? defaults.brave_search,
|
|
128
169
|
model_overrides: parsed.model_overrides || null,
|
|
129
|
-
// v2 fields
|
|
170
|
+
// v2 fields -- passed through directly from config (now from config.local.json)
|
|
130
171
|
current_project: get('current_project') ?? null,
|
|
131
172
|
v2_hint_shown: get('v2_hint_shown') ?? false,
|
|
132
173
|
};
|
|
133
174
|
} catch {
|
|
134
|
-
return { ...defaults, current_project: null, v2_hint_shown: false };
|
|
175
|
+
return { ...defaults, current_project: null, v2_hint_shown: false, sync_hint_shown: false };
|
|
135
176
|
}
|
|
136
177
|
}
|
|
137
178
|
|