gsdd-cli 0.18.4 → 0.19.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 (100) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +625 -607
  3. package/agents/DISTILLATION.md +421 -421
  4. package/agents/README.md +62 -62
  5. package/agents/approach-explorer.md +361 -361
  6. package/agents/debugger.md +82 -82
  7. package/agents/executor.md +394 -394
  8. package/agents/integration-checker.md +318 -318
  9. package/agents/mapper.md +103 -103
  10. package/agents/planner.md +313 -313
  11. package/agents/researcher.md +84 -84
  12. package/agents/roadmapper.md +296 -296
  13. package/agents/synthesizer.md +236 -236
  14. package/agents/verifier.md +337 -337
  15. package/bin/adapters/agents.mjs +34 -34
  16. package/bin/adapters/claude.mjs +191 -191
  17. package/bin/adapters/codex.mjs +85 -85
  18. package/bin/adapters/index.mjs +20 -20
  19. package/bin/adapters/opencode.mjs +278 -278
  20. package/bin/gsdd.mjs +123 -116
  21. package/bin/lib/cli-utils.mjs +28 -28
  22. package/bin/lib/evidence-contract.mjs +112 -112
  23. package/bin/lib/file-ops.mjs +209 -161
  24. package/bin/lib/health-truth.mjs +181 -178
  25. package/bin/lib/health.mjs +265 -235
  26. package/bin/lib/init-flow.mjs +252 -236
  27. package/bin/lib/init-prompts.mjs +248 -247
  28. package/bin/lib/init-runtime.mjs +225 -212
  29. package/bin/lib/init.mjs +17 -17
  30. package/bin/lib/lifecycle-preflight.mjs +360 -333
  31. package/bin/lib/lifecycle-state.mjs +351 -267
  32. package/bin/lib/manifest.mjs +116 -114
  33. package/bin/lib/models.mjs +411 -411
  34. package/bin/lib/phase.mjs +397 -378
  35. package/bin/lib/plan-constants.mjs +30 -30
  36. package/bin/lib/provenance.mjs +109 -106
  37. package/bin/lib/rendering.mjs +178 -130
  38. package/bin/lib/runtime-freshness.mjs +221 -219
  39. package/bin/lib/templates.mjs +225 -224
  40. package/bin/lib/workspace-root.mjs +132 -0
  41. package/distilled/DESIGN.md +2347 -2327
  42. package/distilled/EVIDENCE-INDEX.md +397 -394
  43. package/distilled/README.md +196 -193
  44. package/distilled/SKILL.md +86 -85
  45. package/distilled/templates/agents.block.md +21 -21
  46. package/distilled/templates/agents.md +6 -6
  47. package/distilled/templates/approach.md +232 -232
  48. package/distilled/templates/auth-matrix.md +78 -78
  49. package/distilled/templates/brownfield-change/CHANGE.md +99 -0
  50. package/distilled/templates/brownfield-change/HANDOFF.md +38 -0
  51. package/distilled/templates/brownfield-change/VERIFICATION.md +56 -0
  52. package/distilled/templates/codebase/architecture.md +110 -110
  53. package/distilled/templates/codebase/concerns.md +95 -95
  54. package/distilled/templates/codebase/conventions.md +193 -193
  55. package/distilled/templates/codebase/stack.md +96 -96
  56. package/distilled/templates/delegates/approach-explorer.md +25 -25
  57. package/distilled/templates/delegates/mapper-arch.md +26 -26
  58. package/distilled/templates/delegates/mapper-concerns.md +27 -27
  59. package/distilled/templates/delegates/mapper-quality.md +28 -28
  60. package/distilled/templates/delegates/mapper-tech.md +25 -25
  61. package/distilled/templates/delegates/plan-checker.md +68 -68
  62. package/distilled/templates/delegates/researcher-architecture.md +30 -30
  63. package/distilled/templates/delegates/researcher-features.md +30 -30
  64. package/distilled/templates/delegates/researcher-pitfalls.md +30 -30
  65. package/distilled/templates/delegates/researcher-stack.md +30 -30
  66. package/distilled/templates/delegates/researcher-synthesizer.md +31 -31
  67. package/distilled/templates/research/architecture.md +57 -57
  68. package/distilled/templates/research/features.md +23 -23
  69. package/distilled/templates/research/pitfalls.md +46 -46
  70. package/distilled/templates/research/stack.md +45 -45
  71. package/distilled/templates/research/summary.md +67 -67
  72. package/distilled/templates/roadmap.md +74 -62
  73. package/distilled/templates/spec.md +110 -110
  74. package/distilled/workflows/audit-milestone.md +275 -271
  75. package/distilled/workflows/complete-milestone.md +336 -332
  76. package/distilled/workflows/execute.md +454 -449
  77. package/distilled/workflows/map-codebase.md +253 -253
  78. package/distilled/workflows/new-milestone.md +242 -238
  79. package/distilled/workflows/new-project.md +398 -398
  80. package/distilled/workflows/pause.md +160 -156
  81. package/distilled/workflows/plan-milestone-gaps.md +183 -183
  82. package/distilled/workflows/plan.md +451 -447
  83. package/distilled/workflows/progress.md +227 -223
  84. package/distilled/workflows/quick.md +351 -347
  85. package/distilled/workflows/resume.md +220 -212
  86. package/distilled/workflows/verify-work.md +260 -260
  87. package/distilled/workflows/verify.md +431 -429
  88. package/docs/BROWNFIELD-PROOF.md +95 -95
  89. package/docs/RUNTIME-SUPPORT.md +93 -75
  90. package/docs/USER-GUIDE.md +440 -399
  91. package/docs/VERIFICATION-DISCIPLINE.md +59 -59
  92. package/docs/claude/context-monitor.md +98 -98
  93. package/docs/proof/consumer-node-cli/README.md +37 -37
  94. package/docs/proof/consumer-node-cli/ROADMAP.md +14 -14
  95. package/docs/proof/consumer-node-cli/SPEC.md +17 -17
  96. package/docs/proof/consumer-node-cli/brief.md +9 -9
  97. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-PLAN.md +34 -34
  98. package/docs/proof/consumer-node-cli/phases/01-foundation/01-01-SUMMARY.md +10 -10
  99. package/docs/proof/consumer-node-cli/phases/01-foundation/01-VERIFICATION.md +30 -30
  100. package/package.json +62 -61
@@ -1,224 +1,225 @@
1
- // templates.mjs - Project template and role installation/refresh helpers
2
-
3
- import { existsSync, mkdirSync, readdirSync, cpSync, unlinkSync } from 'fs';
4
- import { join } from 'path';
5
- import { fileHash, readManifest } from './manifest.mjs';
6
-
7
- export function installProjectTemplates({ planningDir, distilledDir, agentsDir }) {
8
- const localTemplatesDir = join(planningDir, 'templates');
9
- const globalTemplatesDir = join(distilledDir, 'templates');
10
-
11
- if (!existsSync(localTemplatesDir)) {
12
- if (existsSync(globalTemplatesDir)) {
13
- cpSync(globalTemplatesDir, localTemplatesDir, { recursive: true });
14
- console.log(' - copied templates to .planning/templates/');
15
- // Warn-only by design: init should not fail on missing templates because
16
- // the user may still proceed and fix later. The hard gate lives in
17
- // `gsdd health` (E6/E7/E8) which reports these as errors. This is the
18
- // first layer of the 3-layer scaffold defense (warn at init, error at
19
- // health, regression tests in manifest suite).
20
- const expectedSubdirs = ['delegates', 'research', 'codebase'];
21
- for (const subdir of expectedSubdirs) {
22
- if (!existsSync(join(localTemplatesDir, subdir))) {
23
- console.log(` - WARN: missing expected template subdir: ${subdir}/`);
24
- }
25
- }
26
- const expectedRootFiles = ['spec.md', 'roadmap.md', 'auth-matrix.md'];
27
- for (const file of expectedRootFiles) {
28
- if (!existsSync(join(localTemplatesDir, file))) {
29
- console.log(` - WARN: missing expected root template file: ${file}`);
30
- }
31
- }
32
- } else {
33
- console.log(' - WARN: missing distilled/templates/; cannot copy templates');
34
- }
35
- } else {
36
- console.log(' - .planning/templates/ already exists');
37
- }
38
-
39
- const localRolesDir = join(localTemplatesDir, 'roles');
40
- if (!existsSync(localRolesDir)) {
41
- if (existsSync(agentsDir)) {
42
- mkdirSync(localRolesDir, { recursive: true });
43
- for (const file of listRoleFiles(agentsDir)) {
44
- cpSync(join(agentsDir, file), join(localRolesDir, file));
45
- }
46
- console.log(' - copied role contracts to .planning/templates/roles/');
47
- } else {
48
- console.log(' - WARN: missing agents/; cannot copy role contracts');
49
- }
50
- } else {
51
- console.log(' - .planning/templates/roles/ already exists');
52
- }
53
- }
54
-
55
- export function refreshTemplates({ planningDir, distilledDir, agentsDir, isDry = false }) {
56
- const existingManifest = readManifest(planningDir);
57
- const globalTemplatesDir = join(distilledDir, 'templates');
58
- const localTemplatesDir = join(planningDir, 'templates');
59
-
60
- const categories = [
61
- { name: 'delegates', src: join(globalTemplatesDir, 'delegates'), dest: join(localTemplatesDir, 'delegates'), manifestKey: 'delegates' },
62
- { name: 'research', src: join(globalTemplatesDir, 'research'), dest: join(localTemplatesDir, 'research'), manifestKey: 'research' },
63
- { name: 'codebase', src: join(globalTemplatesDir, 'codebase'), dest: join(localTemplatesDir, 'codebase'), manifestKey: 'codebase' },
64
- ];
65
-
66
- for (const category of categories) {
67
- refreshCategory(category, existingManifest, isDry);
68
- }
69
-
70
- refreshRootTemplates(globalTemplatesDir, localTemplatesDir, existingManifest, isDry);
71
- refreshRoles(agentsDir, join(localTemplatesDir, 'roles'), existingManifest, isDry);
72
- }
73
-
74
- function listRoleFiles(agentsDir) {
75
- return readdirSync(agentsDir).filter(
76
- (file) => file.endsWith('.md') && file !== 'README.md' && !file.startsWith('_')
77
- );
78
- }
79
-
80
- function refreshCategory({ name, src, dest, manifestKey }, existingManifest, isDry) {
81
- if (!existsSync(src)) return;
82
- if (!existsSync(dest) && !isDry) {
83
- mkdirSync(dest, { recursive: true });
84
- }
85
-
86
- const manifestHashes = existingManifest?.templates?.[manifestKey] || null;
87
- const sourceFiles = readdirSync(src).filter((file) => file.endsWith('.md'));
88
- const installedFiles = existsSync(dest) ? readdirSync(dest).filter((file) => file.endsWith('.md')) : [];
89
-
90
- for (const file of sourceFiles) {
91
- const srcPath = join(src, file);
92
- const destPath = join(dest, file);
93
- const srcHash = fileHash(srcPath);
94
-
95
- if (existsSync(destPath)) {
96
- const destHash = fileHash(destPath);
97
- if (destHash === srcHash) continue;
98
-
99
- const manifestHash = manifestHashes?.[file];
100
- if (manifestHash && destHash !== manifestHash) {
101
- console.log(` - WARN: ${name}/${file} was modified locally; overwriting with framework source`);
102
- }
103
- }
104
-
105
- if (isDry) {
106
- console.log(` - would refresh ${name}/${file}`);
107
- } else {
108
- cpSync(srcPath, destPath);
109
- console.log(` - refreshed ${name}/${file}`);
110
- }
111
- }
112
-
113
- for (const file of installedFiles) {
114
- if (!sourceFiles.includes(file)) {
115
- if (isDry) {
116
- console.log(` - would remove orphan ${name}/${file}`);
117
- } else {
118
- const orphanPath = join(dest, file);
119
- if (existsSync(orphanPath)) {
120
- unlinkSync(orphanPath);
121
- }
122
- console.log(` - removed orphan ${name}/${file}`);
123
- }
124
- }
125
- }
126
- }
127
-
128
- function refreshRootTemplates(globalTemplatesDir, localTemplatesDir, existingManifest, isDry) {
129
- if (!existsSync(globalTemplatesDir)) return;
130
-
131
- const manifestHashes = existingManifest?.templates?.root || null;
132
- const sourceFiles = readdirSync(globalTemplatesDir).filter((file) => file.endsWith('.md'));
133
-
134
- for (const file of sourceFiles) {
135
- const srcPath = join(globalTemplatesDir, file);
136
- const destPath = join(localTemplatesDir, file);
137
- const srcHash = fileHash(srcPath);
138
-
139
- if (existsSync(destPath)) {
140
- const destHash = fileHash(destPath);
141
- if (destHash === srcHash) continue;
142
-
143
- const manifestHash = manifestHashes?.[file];
144
- if (manifestHash && destHash !== manifestHash) {
145
- console.log(` - WARN: templates/${file} was modified locally; overwriting with framework source`);
146
- }
147
- }
148
-
149
- if (isDry) {
150
- console.log(` - would refresh templates/${file}`);
151
- } else {
152
- cpSync(srcPath, destPath);
153
- console.log(` - refreshed templates/${file}`);
154
- }
155
- }
156
-
157
- const installedRootFiles = existsSync(localTemplatesDir)
158
- ? readdirSync(localTemplatesDir).filter((file) => file.endsWith('.md'))
159
- : [];
160
-
161
- for (const file of installedRootFiles) {
162
- if (!sourceFiles.includes(file)) {
163
- if (isDry) {
164
- console.log(` - would remove orphan templates/${file}`);
165
- } else {
166
- const orphanPath = join(localTemplatesDir, file);
167
- if (existsSync(orphanPath)) {
168
- unlinkSync(orphanPath);
169
- }
170
- console.log(` - removed orphan templates/${file}`);
171
- }
172
- }
173
- }
174
- }
175
-
176
- function refreshRoles(agentsDir, localRolesDir, existingManifest, isDry) {
177
- if (!existsSync(agentsDir)) return;
178
- if (!existsSync(localRolesDir) && !isDry) {
179
- mkdirSync(localRolesDir, { recursive: true });
180
- }
181
-
182
- const manifestHashes = existingManifest?.roles || null;
183
- const sourceFiles = listRoleFiles(agentsDir);
184
- const installedFiles = existsSync(localRolesDir)
185
- ? readdirSync(localRolesDir).filter((file) => file.endsWith('.md'))
186
- : [];
187
-
188
- for (const file of sourceFiles) {
189
- const srcPath = join(agentsDir, file);
190
- const destPath = join(localRolesDir, file);
191
- const srcHash = fileHash(srcPath);
192
-
193
- if (existsSync(destPath)) {
194
- const destHash = fileHash(destPath);
195
- if (destHash === srcHash) continue;
196
-
197
- const manifestHash = manifestHashes?.[file];
198
- if (manifestHash && destHash !== manifestHash) {
199
- console.log(` - WARN: roles/${file} was modified locally; overwriting with framework source`);
200
- }
201
- }
202
-
203
- if (isDry) {
204
- console.log(` - would refresh roles/${file}`);
205
- } else {
206
- cpSync(srcPath, destPath);
207
- console.log(` - refreshed roles/${file}`);
208
- }
209
- }
210
-
211
- for (const file of installedFiles) {
212
- if (!sourceFiles.includes(file)) {
213
- if (isDry) {
214
- console.log(` - would remove orphan roles/${file}`);
215
- } else {
216
- const orphanPath = join(localRolesDir, file);
217
- if (existsSync(orphanPath)) {
218
- unlinkSync(orphanPath);
219
- }
220
- console.log(` - removed orphan roles/${file}`);
221
- }
222
- }
223
- }
224
- }
1
+ // templates.mjs - Project template and role installation/refresh helpers
2
+
3
+ import { existsSync, mkdirSync, readdirSync, cpSync, unlinkSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { fileHash, readManifest } from './manifest.mjs';
6
+
7
+ export function installProjectTemplates({ planningDir, distilledDir, agentsDir }) {
8
+ const localTemplatesDir = join(planningDir, 'templates');
9
+ const globalTemplatesDir = join(distilledDir, 'templates');
10
+
11
+ if (!existsSync(localTemplatesDir)) {
12
+ if (existsSync(globalTemplatesDir)) {
13
+ cpSync(globalTemplatesDir, localTemplatesDir, { recursive: true });
14
+ console.log(' - copied templates to .planning/templates/');
15
+ // Warn-only by design: init should not fail on missing templates because
16
+ // the user may still proceed and fix later. The hard gate lives in
17
+ // `gsdd health` (E6/E7/E8) which reports these as errors. This is the
18
+ // first layer of the 3-layer scaffold defense (warn at init, error at
19
+ // health, regression tests in manifest suite).
20
+ const expectedSubdirs = ['delegates', 'research', 'codebase', 'brownfield-change'];
21
+ for (const subdir of expectedSubdirs) {
22
+ if (!existsSync(join(localTemplatesDir, subdir))) {
23
+ console.log(` - WARN: missing expected template subdir: ${subdir}/`);
24
+ }
25
+ }
26
+ const expectedRootFiles = ['spec.md', 'roadmap.md', 'auth-matrix.md'];
27
+ for (const file of expectedRootFiles) {
28
+ if (!existsSync(join(localTemplatesDir, file))) {
29
+ console.log(` - WARN: missing expected root template file: ${file}`);
30
+ }
31
+ }
32
+ } else {
33
+ console.log(' - WARN: missing distilled/templates/; cannot copy templates');
34
+ }
35
+ } else {
36
+ console.log(' - .planning/templates/ already exists');
37
+ }
38
+
39
+ const localRolesDir = join(localTemplatesDir, 'roles');
40
+ if (!existsSync(localRolesDir)) {
41
+ if (existsSync(agentsDir)) {
42
+ mkdirSync(localRolesDir, { recursive: true });
43
+ for (const file of listRoleFiles(agentsDir)) {
44
+ cpSync(join(agentsDir, file), join(localRolesDir, file));
45
+ }
46
+ console.log(' - copied role contracts to .planning/templates/roles/');
47
+ } else {
48
+ console.log(' - WARN: missing agents/; cannot copy role contracts');
49
+ }
50
+ } else {
51
+ console.log(' - .planning/templates/roles/ already exists');
52
+ }
53
+ }
54
+
55
+ export function refreshTemplates({ planningDir, distilledDir, agentsDir, isDry = false }) {
56
+ const existingManifest = readManifest(planningDir);
57
+ const globalTemplatesDir = join(distilledDir, 'templates');
58
+ const localTemplatesDir = join(planningDir, 'templates');
59
+
60
+ const categories = [
61
+ { name: 'delegates', src: join(globalTemplatesDir, 'delegates'), dest: join(localTemplatesDir, 'delegates'), manifestKey: 'delegates' },
62
+ { name: 'research', src: join(globalTemplatesDir, 'research'), dest: join(localTemplatesDir, 'research'), manifestKey: 'research' },
63
+ { name: 'codebase', src: join(globalTemplatesDir, 'codebase'), dest: join(localTemplatesDir, 'codebase'), manifestKey: 'codebase' },
64
+ { name: 'brownfield-change', src: join(globalTemplatesDir, 'brownfield-change'), dest: join(localTemplatesDir, 'brownfield-change'), manifestKey: 'brownfieldChange' },
65
+ ];
66
+
67
+ for (const category of categories) {
68
+ refreshCategory(category, existingManifest, isDry);
69
+ }
70
+
71
+ refreshRootTemplates(globalTemplatesDir, localTemplatesDir, existingManifest, isDry);
72
+ refreshRoles(agentsDir, join(localTemplatesDir, 'roles'), existingManifest, isDry);
73
+ }
74
+
75
+ function listRoleFiles(agentsDir) {
76
+ return readdirSync(agentsDir).filter(
77
+ (file) => file.endsWith('.md') && file !== 'README.md' && !file.startsWith('_')
78
+ );
79
+ }
80
+
81
+ function refreshCategory({ name, src, dest, manifestKey }, existingManifest, isDry) {
82
+ if (!existsSync(src)) return;
83
+ if (!existsSync(dest) && !isDry) {
84
+ mkdirSync(dest, { recursive: true });
85
+ }
86
+
87
+ const manifestHashes = existingManifest?.templates?.[manifestKey] || null;
88
+ const sourceFiles = readdirSync(src).filter((file) => file.endsWith('.md'));
89
+ const installedFiles = existsSync(dest) ? readdirSync(dest).filter((file) => file.endsWith('.md')) : [];
90
+
91
+ for (const file of sourceFiles) {
92
+ const srcPath = join(src, file);
93
+ const destPath = join(dest, file);
94
+ const srcHash = fileHash(srcPath);
95
+
96
+ if (existsSync(destPath)) {
97
+ const destHash = fileHash(destPath);
98
+ if (destHash === srcHash) continue;
99
+
100
+ const manifestHash = manifestHashes?.[file];
101
+ if (manifestHash && destHash !== manifestHash) {
102
+ console.log(` - WARN: ${name}/${file} was modified locally; overwriting with framework source`);
103
+ }
104
+ }
105
+
106
+ if (isDry) {
107
+ console.log(` - would refresh ${name}/${file}`);
108
+ } else {
109
+ cpSync(srcPath, destPath);
110
+ console.log(` - refreshed ${name}/${file}`);
111
+ }
112
+ }
113
+
114
+ for (const file of installedFiles) {
115
+ if (!sourceFiles.includes(file)) {
116
+ if (isDry) {
117
+ console.log(` - would remove orphan ${name}/${file}`);
118
+ } else {
119
+ const orphanPath = join(dest, file);
120
+ if (existsSync(orphanPath)) {
121
+ unlinkSync(orphanPath);
122
+ }
123
+ console.log(` - removed orphan ${name}/${file}`);
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ function refreshRootTemplates(globalTemplatesDir, localTemplatesDir, existingManifest, isDry) {
130
+ if (!existsSync(globalTemplatesDir)) return;
131
+
132
+ const manifestHashes = existingManifest?.templates?.root || null;
133
+ const sourceFiles = readdirSync(globalTemplatesDir).filter((file) => file.endsWith('.md'));
134
+
135
+ for (const file of sourceFiles) {
136
+ const srcPath = join(globalTemplatesDir, file);
137
+ const destPath = join(localTemplatesDir, file);
138
+ const srcHash = fileHash(srcPath);
139
+
140
+ if (existsSync(destPath)) {
141
+ const destHash = fileHash(destPath);
142
+ if (destHash === srcHash) continue;
143
+
144
+ const manifestHash = manifestHashes?.[file];
145
+ if (manifestHash && destHash !== manifestHash) {
146
+ console.log(` - WARN: templates/${file} was modified locally; overwriting with framework source`);
147
+ }
148
+ }
149
+
150
+ if (isDry) {
151
+ console.log(` - would refresh templates/${file}`);
152
+ } else {
153
+ cpSync(srcPath, destPath);
154
+ console.log(` - refreshed templates/${file}`);
155
+ }
156
+ }
157
+
158
+ const installedRootFiles = existsSync(localTemplatesDir)
159
+ ? readdirSync(localTemplatesDir).filter((file) => file.endsWith('.md'))
160
+ : [];
161
+
162
+ for (const file of installedRootFiles) {
163
+ if (!sourceFiles.includes(file)) {
164
+ if (isDry) {
165
+ console.log(` - would remove orphan templates/${file}`);
166
+ } else {
167
+ const orphanPath = join(localTemplatesDir, file);
168
+ if (existsSync(orphanPath)) {
169
+ unlinkSync(orphanPath);
170
+ }
171
+ console.log(` - removed orphan templates/${file}`);
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ function refreshRoles(agentsDir, localRolesDir, existingManifest, isDry) {
178
+ if (!existsSync(agentsDir)) return;
179
+ if (!existsSync(localRolesDir) && !isDry) {
180
+ mkdirSync(localRolesDir, { recursive: true });
181
+ }
182
+
183
+ const manifestHashes = existingManifest?.roles || null;
184
+ const sourceFiles = listRoleFiles(agentsDir);
185
+ const installedFiles = existsSync(localRolesDir)
186
+ ? readdirSync(localRolesDir).filter((file) => file.endsWith('.md'))
187
+ : [];
188
+
189
+ for (const file of sourceFiles) {
190
+ const srcPath = join(agentsDir, file);
191
+ const destPath = join(localRolesDir, file);
192
+ const srcHash = fileHash(srcPath);
193
+
194
+ if (existsSync(destPath)) {
195
+ const destHash = fileHash(destPath);
196
+ if (destHash === srcHash) continue;
197
+
198
+ const manifestHash = manifestHashes?.[file];
199
+ if (manifestHash && destHash !== manifestHash) {
200
+ console.log(` - WARN: roles/${file} was modified locally; overwriting with framework source`);
201
+ }
202
+ }
203
+
204
+ if (isDry) {
205
+ console.log(` - would refresh roles/${file}`);
206
+ } else {
207
+ cpSync(srcPath, destPath);
208
+ console.log(` - refreshed roles/${file}`);
209
+ }
210
+ }
211
+
212
+ for (const file of installedFiles) {
213
+ if (!sourceFiles.includes(file)) {
214
+ if (isDry) {
215
+ console.log(` - would remove orphan roles/${file}`);
216
+ } else {
217
+ const orphanPath = join(localRolesDir, file);
218
+ if (existsSync(orphanPath)) {
219
+ unlinkSync(orphanPath);
220
+ }
221
+ console.log(` - removed orphan roles/${file}`);
222
+ }
223
+ }
224
+ }
225
+ }
@@ -0,0 +1,132 @@
1
+ import { existsSync } from 'fs';
2
+ import { dirname, join, resolve } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ function normalizePath(value, cwd) {
6
+ return resolve(cwd, String(value));
7
+ }
8
+
9
+ function hasPlanningMarker(root) {
10
+ return existsSync(join(root, '.planning', 'config.json')) || existsSync(join(root, '.planning'));
11
+ }
12
+
13
+ export function consumeWorkspaceRootArg(rawArgs = []) {
14
+ const args = [];
15
+ let workspaceRootArg = null;
16
+ let invalid = false;
17
+
18
+ for (let index = 0; index < rawArgs.length; index += 1) {
19
+ const arg = rawArgs[index];
20
+ if (arg !== '--workspace-root') {
21
+ args.push(arg);
22
+ continue;
23
+ }
24
+
25
+ const value = rawArgs[index + 1] ?? null;
26
+ if (!value || value.startsWith('--')) {
27
+ invalid = true;
28
+ continue;
29
+ }
30
+
31
+ workspaceRootArg = value;
32
+ index += 1;
33
+ }
34
+
35
+ return { args, workspaceRootArg, invalid };
36
+ }
37
+
38
+ export function findWorkspaceRoot(startDir = process.cwd()) {
39
+ let current = resolve(startDir);
40
+
41
+ while (true) {
42
+ if (hasPlanningMarker(current)) return current;
43
+ const parent = dirname(current);
44
+ if (parent === current) return null;
45
+ current = parent;
46
+ }
47
+ }
48
+
49
+ export function deriveWorkspaceRootFromHelperLocation(entryFileUrl) {
50
+ if (!entryFileUrl) return null;
51
+
52
+ const entryPath = entryFileUrl.startsWith('file:')
53
+ ? fileURLToPath(entryFileUrl)
54
+ : resolve(String(entryFileUrl));
55
+ const binDir = dirname(entryPath);
56
+ const planningDir = dirname(binDir);
57
+ const workspaceRoot = dirname(planningDir);
58
+
59
+ if (hasPlanningMarker(workspaceRoot) && binDir === join(planningDir, 'bin')) {
60
+ return workspaceRoot;
61
+ }
62
+
63
+ return null;
64
+ }
65
+
66
+ export function resolveWorkspaceContext(rawArgs = [], { cwd = process.cwd(), env = process.env } = {}) {
67
+ const { args, workspaceRootArg, invalid } = consumeWorkspaceRootArg(rawArgs);
68
+ if (invalid) {
69
+ return {
70
+ args,
71
+ invalid: true,
72
+ error: 'Usage: --workspace-root <path>',
73
+ workspaceRoot: resolve(cwd),
74
+ planningDir: join(resolve(cwd), '.planning'),
75
+ };
76
+ }
77
+
78
+ if (workspaceRootArg) {
79
+ const explicitRoot = normalizePath(workspaceRootArg, cwd);
80
+ if (!hasPlanningMarker(explicitRoot)) {
81
+ return {
82
+ args,
83
+ invalid: true,
84
+ error: `Workspace root does not contain .planning/: ${workspaceRootArg}`,
85
+ workspaceRoot: explicitRoot,
86
+ planningDir: join(explicitRoot, '.planning'),
87
+ };
88
+ }
89
+ }
90
+
91
+ const candidates = [];
92
+
93
+ if (workspaceRootArg) candidates.push(normalizePath(workspaceRootArg, cwd));
94
+
95
+ const discovered = findWorkspaceRoot(cwd);
96
+ if (discovered) candidates.push(discovered);
97
+
98
+ if (env.GSDD_WORKSPACE_ROOT) candidates.push(normalizePath(env.GSDD_WORKSPACE_ROOT, cwd));
99
+
100
+ candidates.push(resolve(cwd));
101
+
102
+ for (const candidate of candidates) {
103
+ if (hasPlanningMarker(candidate)) {
104
+ return {
105
+ args,
106
+ invalid: false,
107
+ workspaceRoot: candidate,
108
+ planningDir: join(candidate, '.planning'),
109
+ };
110
+ }
111
+ }
112
+
113
+ const fallbackRoot = candidates[0] ?? resolve(cwd);
114
+ return {
115
+ args,
116
+ invalid: false,
117
+ workspaceRoot: fallbackRoot,
118
+ planningDir: join(fallbackRoot, '.planning'),
119
+ };
120
+ }
121
+
122
+ export function bootstrapHelperWorkspace(entryFileUrl, env = process.env) {
123
+ const helperRoot = deriveWorkspaceRootFromHelperLocation(entryFileUrl);
124
+ if (!helperRoot) return null;
125
+ env.GSDD_WORKSPACE_ROOT = helperRoot;
126
+ try {
127
+ process.chdir(helperRoot);
128
+ } catch {
129
+ // best-effort: commands also resolve from env/upward search
130
+ }
131
+ return helperRoot;
132
+ }