orbital-command 0.3.0 → 1.0.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 (160) hide show
  1. package/README.md +67 -42
  2. package/bin/commands/config.js +19 -0
  3. package/bin/commands/events.js +40 -0
  4. package/bin/commands/launch.js +126 -0
  5. package/bin/commands/manifest.js +283 -0
  6. package/bin/commands/registry.js +104 -0
  7. package/bin/commands/update.js +24 -0
  8. package/bin/lib/helpers.js +229 -0
  9. package/bin/orbital.js +95 -870
  10. package/dist/assets/Landing-CfQdHR0N.js +11 -0
  11. package/dist/assets/PrimitivesConfig-DThSipFy.js +32 -0
  12. package/dist/assets/QualityGates-B4kxM5UU.js +26 -0
  13. package/dist/assets/SessionTimeline-Bz1iZnmg.js +1 -0
  14. package/dist/assets/Settings-DLcZwbCT.js +12 -0
  15. package/dist/assets/SourceControl-BMNIz7Lt.js +36 -0
  16. package/dist/assets/WorkflowVisualizer-CxuSBOYu.js +69 -0
  17. package/dist/assets/{arrow-down-CPy85_J6.js → arrow-down-DVPp6_qp.js} +1 -1
  18. package/dist/assets/bot-NFaJBDn_.js +6 -0
  19. package/dist/assets/{charts-DbDg0Psc.js → charts-LGLb8hyU.js} +1 -1
  20. package/dist/assets/{circle-x-Cwz6ZQDV.js → circle-x-IsFCkBZu.js} +1 -1
  21. package/dist/assets/{file-text-C46Xr65c.js → file-text-J1cebZXF.js} +1 -1
  22. package/dist/assets/{globe-Cn2yNZUD.js → globe-WzeyHsUc.js} +1 -1
  23. package/dist/assets/index-BdJ57EhC.css +1 -0
  24. package/dist/assets/index-o4ScMAuR.js +349 -0
  25. package/dist/assets/{key-OPaNTWJ5.js → key-CKR8JJSj.js} +1 -1
  26. package/dist/assets/{minus-GMsbpKym.js → minus-CHBsJyjp.js} +1 -1
  27. package/dist/assets/radio-xqZaR-Uk.js +6 -0
  28. package/dist/assets/rocket-D_xvvNG6.js +6 -0
  29. package/dist/assets/{shield-DwAFkDYI.js → shield-TdB1yv_a.js} +1 -1
  30. package/dist/assets/useSocketListener-0L5yiN5i.js +1 -0
  31. package/dist/assets/useWorkflowEditor-CqeRWVQX.js +11 -0
  32. package/dist/assets/workflow-constants-Rw-GmgHZ.js +6 -0
  33. package/dist/assets/zap-C9wqYMpl.js +6 -0
  34. package/dist/index.html +3 -3
  35. package/dist/server/server/__tests__/data-routes.test.js +2 -0
  36. package/dist/server/server/__tests__/scope-routes.test.js +1 -0
  37. package/dist/server/server/config-migrator.js +0 -3
  38. package/dist/server/server/config.js +35 -6
  39. package/dist/server/server/database.js +0 -22
  40. package/dist/server/server/index.js +26 -814
  41. package/dist/server/server/init.js +32 -399
  42. package/dist/server/server/launch.js +1 -1
  43. package/dist/server/server/parsers/event-parser.js +4 -1
  44. package/dist/server/server/project-context.js +19 -9
  45. package/dist/server/server/project-manager.js +6 -6
  46. package/dist/server/server/routes/aggregate-routes.js +871 -0
  47. package/dist/server/server/routes/config-routes.js +41 -88
  48. package/dist/server/server/routes/data-routes.js +5 -15
  49. package/dist/server/server/routes/dispatch-routes.js +24 -8
  50. package/dist/server/server/routes/manifest-routes.js +1 -1
  51. package/dist/server/server/routes/scope-routes.js +10 -7
  52. package/dist/server/server/schema.js +1 -0
  53. package/dist/server/server/services/batch-orchestrator.js +17 -3
  54. package/dist/server/server/services/config-service.js +10 -1
  55. package/dist/server/server/services/scope-service.js +7 -7
  56. package/dist/server/server/services/sprint-orchestrator.js +24 -11
  57. package/dist/server/server/services/sprint-service.js +2 -2
  58. package/dist/server/server/uninstall.js +195 -0
  59. package/dist/server/server/update.js +212 -0
  60. package/dist/server/server/utils/dispatch-utils.js +8 -6
  61. package/dist/server/server/utils/flag-builder.js +54 -0
  62. package/dist/server/server/utils/json-fields.js +14 -0
  63. package/dist/server/server/utils/json-fields.test.js +73 -0
  64. package/dist/server/server/utils/route-helpers.js +37 -0
  65. package/dist/server/server/utils/route-helpers.test.js +115 -0
  66. package/dist/server/server/watchers/event-watcher.js +28 -13
  67. package/dist/server/server/wizard/config-editor.js +4 -4
  68. package/dist/server/server/wizard/doctor.js +2 -2
  69. package/dist/server/server/wizard/index.js +224 -39
  70. package/dist/server/server/wizard/phases/welcome.js +1 -4
  71. package/dist/server/server/wizard/ui.js +6 -7
  72. package/dist/server/shared/api-types.js +80 -1
  73. package/dist/server/shared/workflow-engine.js +1 -1
  74. package/package.json +20 -20
  75. package/schemas/orbital.config.schema.json +1 -19
  76. package/scripts/postinstall.js +6 -42
  77. package/scripts/release.sh +53 -0
  78. package/server/__tests__/data-routes.test.ts +2 -0
  79. package/server/__tests__/scope-routes.test.ts +1 -0
  80. package/server/config-migrator.ts +0 -3
  81. package/server/config.ts +39 -11
  82. package/server/database.ts +0 -26
  83. package/server/global-config.ts +4 -0
  84. package/server/index.ts +29 -894
  85. package/server/init.ts +32 -443
  86. package/server/launch.ts +1 -1
  87. package/server/parsers/event-parser.ts +4 -1
  88. package/server/project-context.ts +26 -10
  89. package/server/project-manager.ts +5 -6
  90. package/server/routes/aggregate-routes.ts +968 -0
  91. package/server/routes/config-routes.ts +41 -81
  92. package/server/routes/data-routes.ts +7 -16
  93. package/server/routes/dispatch-routes.ts +29 -8
  94. package/server/routes/manifest-routes.ts +1 -1
  95. package/server/routes/scope-routes.ts +12 -7
  96. package/server/schema.ts +1 -0
  97. package/server/services/batch-orchestrator.ts +18 -2
  98. package/server/services/config-service.ts +10 -1
  99. package/server/services/scope-service.ts +6 -6
  100. package/server/services/sprint-orchestrator.ts +24 -9
  101. package/server/services/sprint-service.ts +2 -2
  102. package/server/uninstall.ts +214 -0
  103. package/server/update.ts +263 -0
  104. package/server/utils/dispatch-utils.ts +8 -6
  105. package/server/utils/flag-builder.ts +56 -0
  106. package/server/utils/json-fields.test.ts +83 -0
  107. package/server/utils/json-fields.ts +14 -0
  108. package/server/utils/route-helpers.test.ts +144 -0
  109. package/server/utils/route-helpers.ts +38 -0
  110. package/server/watchers/event-watcher.ts +24 -12
  111. package/server/wizard/config-editor.ts +4 -4
  112. package/server/wizard/doctor.ts +2 -2
  113. package/server/wizard/index.ts +291 -40
  114. package/server/wizard/phases/welcome.ts +1 -5
  115. package/server/wizard/ui.ts +6 -7
  116. package/shared/api-types.ts +106 -0
  117. package/shared/workflow-engine.ts +1 -1
  118. package/templates/agents/QUICK-REFERENCE.md +1 -0
  119. package/templates/agents/README.md +1 -0
  120. package/templates/agents/SKILL-TRIGGERS.md +11 -0
  121. package/templates/agents/green-team/deep-dive.md +361 -0
  122. package/templates/hooks/end-session.sh +1 -0
  123. package/templates/hooks/init-session.sh +1 -0
  124. package/templates/hooks/scope-commit-logger.sh +2 -2
  125. package/templates/hooks/scope-create-gate.sh +2 -4
  126. package/templates/hooks/scope-gate.sh +4 -6
  127. package/templates/hooks/scope-helpers.sh +10 -1
  128. package/templates/hooks/scope-lifecycle-gate.sh +14 -5
  129. package/templates/hooks/scope-prepare.sh +1 -1
  130. package/templates/hooks/scope-transition.sh +14 -6
  131. package/templates/hooks/time-tracker.sh +2 -5
  132. package/templates/orbital.config.json +1 -4
  133. package/templates/presets/development.json +4 -4
  134. package/templates/presets/gitflow.json +7 -0
  135. package/templates/prompts/README.md +23 -0
  136. package/templates/prompts/deep-dive-audit.md +94 -0
  137. package/templates/quick/rules.md +56 -5
  138. package/templates/skills/git-commit/SKILL.md +21 -6
  139. package/templates/skills/git-dev/SKILL.md +8 -4
  140. package/templates/skills/git-main/SKILL.md +8 -4
  141. package/templates/skills/git-production/SKILL.md +6 -3
  142. package/templates/skills/git-staging/SKILL.md +6 -3
  143. package/templates/skills/scope-fix-review/SKILL.md +8 -4
  144. package/templates/skills/scope-implement/SKILL.md +13 -5
  145. package/templates/skills/scope-post-review/SKILL.md +16 -4
  146. package/templates/skills/scope-pre-review/SKILL.md +6 -2
  147. package/dist/assets/PrimitivesConfig-CrmQXYh4.js +0 -32
  148. package/dist/assets/QualityGates-BbasOsF3.js +0 -21
  149. package/dist/assets/SessionTimeline-CGeJsVvy.js +0 -1
  150. package/dist/assets/Settings-oiM496mc.js +0 -12
  151. package/dist/assets/SourceControl-B1fP2nJL.js +0 -41
  152. package/dist/assets/WorkflowVisualizer-CWLYf-f0.js +0 -74
  153. package/dist/assets/formatDistanceToNow-BMqsSP44.js +0 -1
  154. package/dist/assets/index-Aj4sV8Al.css +0 -1
  155. package/dist/assets/index-Bc9dK3MW.js +0 -354
  156. package/dist/assets/useWorkflowEditor-BJkTX_NR.js +0 -16
  157. package/dist/assets/zap-DfbUoOty.js +0 -11
  158. package/dist/server/server/services/telemetry-service.js +0 -143
  159. package/server/services/telemetry-service.ts +0 -195
  160. /package/{shared/default-workflow.json → templates/presets/default.json} +0 -0
@@ -10,10 +10,10 @@
10
10
 
11
11
  import fs from 'fs';
12
12
  import path from 'path';
13
- import crypto from 'crypto';
13
+ import { spawn, execFileSync } from 'child_process';
14
14
  import * as p from '@clack/prompts';
15
15
  import pc from 'picocolors';
16
- import { buildSetupState, buildProjectState, ORBITAL_HOME } from './detect.js';
16
+ import { buildSetupState, buildProjectState } from './detect.js';
17
17
  import { phaseSetupWizard } from './phases/setup-wizard.js';
18
18
  import { phaseWelcome } from './phases/welcome.js';
19
19
  import { phaseProjectSetup } from './phases/project-setup.js';
@@ -22,6 +22,8 @@ import { phaseConfirm, showPostInstall } from './phases/confirm.js';
22
22
  import { NOTES } from './ui.js';
23
23
  import { runConfigEditor } from './config-editor.js';
24
24
  import { runDoctor } from './doctor.js';
25
+ import { isITerm2Available } from '../adapters/iterm2-adapter.js';
26
+ import { registerProject } from '../global-config.js';
25
27
 
26
28
  export { runConfigEditor, runDoctor };
27
29
 
@@ -50,8 +52,8 @@ export async function runSetupWizard(packageVersion: string): Promise<void> {
50
52
 
51
53
  p.outro(
52
54
  state.linkedProjects.length > 0
53
- ? `Run ${pc.cyan('orbital launch --open')} to open the dashboard.`
54
- : `Run ${pc.cyan('orbital init')} in a project directory to get started.`
55
+ ? `Run ${pc.cyan('orbital')} to launch the dashboard.`
56
+ : `Run ${pc.cyan('orbital')} in a project directory to get started.`
55
57
  );
56
58
  }
57
59
 
@@ -73,7 +75,7 @@ export async function runProjectSetup(projectRoot: string, packageVersion: strin
73
75
 
74
76
  await runProjectPhases(state, useForce);
75
77
 
76
- p.outro(`Run ${pc.cyan('orbital launch --open')} to open the dashboard.`);
78
+ p.outro(`Run ${pc.cyan('orbital')} to launch the dashboard.`);
77
79
  }
78
80
 
79
81
  // ─── Shared project phases (used by both flows) ────────────────
@@ -104,7 +106,7 @@ async function runProjectPhases(state: ReturnType<typeof buildProjectState>, use
104
106
  commands: state.selectedCommands,
105
107
  });
106
108
 
107
- registerProject(state.projectRoot, state.projectName);
109
+ registerProject(state.projectRoot, { name: state.projectName });
108
110
  stampTemplateVersion(state.projectRoot, state.packageVersion);
109
111
 
110
112
  s.stop('Project ready.');
@@ -128,47 +130,294 @@ async function runProjectSetupInline(projectRoot: string, packageVersion: string
128
130
  await runProjectPhases(state, false);
129
131
  }
130
132
 
131
- // ─── Registration ──────────────────────────────────────────────
133
+ // ─── Update Check ─────────────────────────────────────────────
132
134
 
133
- function registerProject(projectRoot: string, projectName?: string): void {
134
- const registryPath = path.join(ORBITAL_HOME, 'config.json');
135
- let registry: { version: number; projects: Array<Record<string, unknown>> };
135
+ interface UpdateInfo {
136
+ current: string;
137
+ latest: string;
138
+ isOutdated: boolean;
139
+ }
140
+
141
+ async function checkForUpdate(
142
+ currentVersion: string,
143
+ cache: { lastUpdateCheck?: string; latestVersion?: string },
144
+ ): Promise<{ info: UpdateInfo | null; cacheChanged: boolean }> {
145
+ // Use cache if checked within 24 hours
146
+ if (cache.lastUpdateCheck && cache.latestVersion) {
147
+ const age = Date.now() - new Date(cache.lastUpdateCheck).getTime();
148
+ if (age < 24 * 60 * 60 * 1000) {
149
+ const isOutdated = cache.latestVersion !== currentVersion;
150
+ return {
151
+ info: { current: currentVersion, latest: cache.latestVersion, isOutdated },
152
+ cacheChanged: false,
153
+ };
154
+ }
155
+ }
136
156
 
157
+ // Fetch from npm registry
137
158
  try {
138
- registry = fs.existsSync(registryPath)
139
- ? JSON.parse(fs.readFileSync(registryPath, 'utf8'))
140
- : { version: 1, projects: [] };
159
+ const res = await fetch('https://registry.npmjs.org/orbital-command/latest', {
160
+ signal: AbortSignal.timeout(3000),
161
+ });
162
+ const data = await res.json() as { version: string };
163
+ const latest = data.version;
164
+ return {
165
+ info: { current: currentVersion, latest, isOutdated: latest !== currentVersion },
166
+ cacheChanged: true,
167
+ };
141
168
  } catch {
142
- registry = { version: 1, projects: [] };
169
+ return { info: null, cacheChanged: false };
143
170
  }
171
+ }
144
172
 
145
- if (!registry.projects) registry.projects = [];
146
- if (registry.projects.some((proj: Record<string, unknown>) => proj.path === projectRoot)) return;
147
-
148
- const COLORS = [
149
- '210 80% 55%', '340 75% 55%', '160 60% 45%', '30 90% 55%',
150
- '270 65% 55%', '50 85% 50%', '180 55% 45%', '0 70% 55%',
151
- ];
152
- const usedColors = registry.projects.map((proj: Record<string, unknown>) => proj.color);
153
- const color = COLORS.find(c => !usedColors.includes(c)) || COLORS[0];
154
-
155
- const name = projectName || path.basename(projectRoot);
156
- const baseSlug = path.basename(projectRoot).toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') || 'project';
157
- const existingIds = registry.projects.map((proj: Record<string, unknown>) => proj.id);
158
- const slug = existingIds.includes(baseSlug)
159
- ? `${baseSlug}-${crypto.createHash('sha256').update(projectRoot).digest('hex').slice(0, 4)}`
160
- : baseSlug;
161
-
162
- registry.projects.push({
163
- id: slug,
164
- path: projectRoot,
165
- name,
166
- color,
167
- registeredAt: new Date().toISOString(),
168
- enabled: true,
173
+ // ─── Hub Menu ─────────────────────────────────────────────────
174
+
175
+ export type HubAction = 'launch' | 'init' | 'config' | 'doctor' | 'update' | 'status' | 'reset';
176
+
177
+ export interface HubResult {
178
+ action: HubAction;
179
+ setItermPromptShown?: boolean;
180
+ updateCache?: { lastUpdateCheck: string; latestVersion?: string };
181
+ }
182
+
183
+ /**
184
+ * Context-aware hub menu — the main entry point for `orbital` (no args).
185
+ * Checks for updates, offers template sync, shows iTerm2 recommendation, then menu.
186
+ */
187
+ export async function runHub(opts: {
188
+ packageVersion: string;
189
+ isProjectInitialized: boolean;
190
+ projectNames: string[];
191
+ itermPromptShown: boolean;
192
+ isMac: boolean;
193
+ lastUpdateCheck?: string;
194
+ latestVersion?: string;
195
+ projectPaths: Array<{ name: string; path: string }>;
196
+ }): Promise<HubResult> {
197
+ const result: HubResult = { action: 'launch' };
198
+
199
+ p.intro(`${pc.bgCyan(pc.black(' Orbital Command '))} ${pc.dim(`v${opts.packageVersion}`)}`);
200
+
201
+ // ── Update check ──
202
+ const updateCheck = await checkForUpdate(opts.packageVersion, {
203
+ lastUpdateCheck: opts.lastUpdateCheck,
204
+ latestVersion: opts.latestVersion,
169
205
  });
170
206
 
171
- fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2), 'utf8');
207
+ if (updateCheck.cacheChanged) {
208
+ result.updateCache = {
209
+ lastUpdateCheck: new Date().toISOString(),
210
+ latestVersion: updateCheck.info?.latest,
211
+ };
212
+ }
213
+
214
+ if (updateCheck.info?.isOutdated) {
215
+ p.log.info(
216
+ `Update available: ${pc.dim(`v${updateCheck.info.current}`)} → ${pc.cyan(`v${updateCheck.info.latest}`)}`
217
+ );
218
+
219
+ const updateChoice = await p.select({
220
+ message: 'Update Orbital Command now?',
221
+ options: [
222
+ { value: 'update', label: 'Yes, update' },
223
+ { value: 'skip', label: 'Skip for now' },
224
+ ],
225
+ });
226
+
227
+ if (!p.isCancel(updateChoice) && updateChoice === 'update') {
228
+ const s = p.spinner();
229
+ s.start('Updating Orbital Command...');
230
+ try {
231
+ execFileSync('npm', ['update', '-g', 'orbital-command'], { stdio: 'pipe', timeout: 60000 });
232
+ s.stop(`Updated to v${updateCheck.info.latest}!`);
233
+ p.outro(`Run ${pc.cyan('orbital')} again to use the new version.`);
234
+ process.exit(0);
235
+ } catch (err) {
236
+ s.stop('Update failed.');
237
+ const msg = err instanceof Error ? err.message : String(err);
238
+ if (msg.includes('EACCES') || msg.includes('permission denied')) {
239
+ p.log.error('Permission denied. Try running with sudo or ensure npm is installed via nvm.');
240
+ } else if (msg.includes('ETIMEDOUT') || msg.includes('timeout')) {
241
+ p.log.error('Update timed out. Check your network connection and try again.');
242
+ } else {
243
+ p.log.error(msg);
244
+ }
245
+ }
246
+ }
247
+ }
248
+
249
+ // ── Template staleness check ──
250
+ if (opts.projectPaths.length > 0) {
251
+ const mod = await import('../manifest.js');
252
+ const initMod = await import('../init.js');
253
+ const outdatedProjects: Array<{
254
+ name: string;
255
+ path: string;
256
+ details: string[];
257
+ }> = [];
258
+
259
+ for (const proj of opts.projectPaths) {
260
+ if (!fs.existsSync(proj.path)) {
261
+ p.log.warn(`${proj.name}: project path not found (${proj.path})`);
262
+ continue;
263
+ }
264
+ const manifest = mod.loadManifest(proj.path);
265
+ if (!manifest) continue;
266
+ const claudeDir = path.join(proj.path, '.claude');
267
+ mod.refreshFileStatuses(manifest, claudeDir);
268
+ const summary = mod.summarizeManifest(manifest);
269
+ const parts = Object.entries(summary.byType)
270
+ .filter(([, counts]) => counts.outdated > 0)
271
+ .map(([type, counts]) => `${counts.outdated} ${type}`);
272
+ if (parts.length > 0) {
273
+ outdatedProjects.push({ name: proj.name, path: proj.path, details: parts });
274
+ }
275
+ }
276
+
277
+ if (outdatedProjects.length > 0) {
278
+ const lines = outdatedProjects.map(proj =>
279
+ ` ${pc.cyan(proj.name.padEnd(16))} ${proj.details.join(', ')} outdated`
280
+ );
281
+ const count = outdatedProjects.length;
282
+ p.note(lines.join('\n'), `${count} project${count > 1 ? 's have' : ' has'} outdated templates`);
283
+
284
+ const syncChoice = await p.select({
285
+ message: 'Update project templates now?',
286
+ options: [
287
+ { value: 'update', label: 'Yes, update all safe files', hint: 'skips modified and pinned' },
288
+ { value: 'skip', label: 'Skip for now' },
289
+ ],
290
+ });
291
+
292
+ if (!p.isCancel(syncChoice) && syncChoice === 'update') {
293
+ for (const proj of outdatedProjects) {
294
+ const s = p.spinner();
295
+ s.start(`Updating ${proj.name}...`);
296
+ try {
297
+ initMod.runUpdate(proj.path, { dryRun: false });
298
+ s.stop(`${proj.name} updated.`);
299
+ } catch (err) {
300
+ s.stop(`${proj.name} failed.`);
301
+ p.log.warn(err instanceof Error ? err.message : String(err));
302
+ }
303
+ }
304
+ }
305
+ }
306
+ }
307
+
308
+ // ── iTerm2 recommendation (macOS only, one-time) ──
309
+ if (opts.isMac && !opts.itermPromptShown && !isITerm2Available()) {
310
+ p.note(
311
+ `Sprint dispatch, batch orchestration, and session management\n` +
312
+ `use iTerm2 tabs to run parallel Claude Code sessions.\n` +
313
+ `Without it, sessions fall back to basic subprocess mode.`,
314
+ 'iTerm2 Recommended',
315
+ );
316
+
317
+ const itermChoice = await p.select({
318
+ message: 'Install iTerm2?',
319
+ options: [
320
+ { value: 'install', label: 'Open download page', hint: 'https://iterm2.com' },
321
+ { value: 'skip', label: 'Skip for now' },
322
+ ],
323
+ });
324
+
325
+ result.setItermPromptShown = true;
326
+
327
+ if (!p.isCancel(itermChoice) && itermChoice === 'install') {
328
+ spawn('open', ['https://iterm2.com'], { detached: true, stdio: 'ignore' }).unref();
329
+ p.log.info('Waiting for iTerm2 to install... (press any key to skip)');
330
+
331
+ await new Promise<void>((resolve) => {
332
+ let done = false;
333
+ const cleanup = (): void => {
334
+ if (done) return;
335
+ done = true;
336
+ process.stdin.setRawMode?.(false);
337
+ process.stdin.removeListener('data', onKey);
338
+ process.stdin.pause();
339
+ clearInterval(timer);
340
+ resolve();
341
+ };
342
+ const onKey = (): void => { cleanup(); };
343
+ const startTime = Date.now();
344
+ const MAX_WAIT = 10 * 60 * 1000; // 10 minutes
345
+ const timer = setInterval(() => {
346
+ if (isITerm2Available()) {
347
+ p.log.success('iTerm2 detected!');
348
+ cleanup();
349
+ } else if (Date.now() - startTime > MAX_WAIT) {
350
+ cleanup();
351
+ }
352
+ }, 3000);
353
+ process.stdin.setRawMode?.(true);
354
+ process.stdin.resume();
355
+ process.stdin.on('data', onKey);
356
+ });
357
+ }
358
+ }
359
+
360
+ // ── Build menu options based on project state ──
361
+ const projectHint = opts.projectNames.length > 0
362
+ ? pc.dim(` (${opts.projectNames.join(', ')})`)
363
+ : '';
364
+
365
+ const options: Array<{ value: HubAction; label: string; hint?: string }> = [];
366
+
367
+ if (opts.isProjectInitialized) {
368
+ options.push(
369
+ { value: 'launch', label: `Launch dashboard${projectHint}` },
370
+ { value: 'config', label: 'Config', hint: 'modify project settings' },
371
+ { value: 'doctor', label: 'Doctor', hint: 'health check & diagnostics' },
372
+ { value: 'update', label: 'Update templates', hint: 'sync to latest' },
373
+ { value: 'status', label: 'Status', hint: 'template sync status' },
374
+ { value: 'reset', label: 'Reset to defaults', hint: 'force-reset all templates' },
375
+ );
376
+ } else {
377
+ options.push(
378
+ { value: 'init', label: 'Initialize this project' },
379
+ { value: 'launch', label: `Launch dashboard${projectHint}` },
380
+ );
381
+ }
382
+
383
+ const action = await p.select({
384
+ message: 'What would you like to do?',
385
+ options,
386
+ });
387
+
388
+ if (p.isCancel(action)) {
389
+ p.cancel('Cancelled.');
390
+ process.exit(0);
391
+ }
392
+
393
+ // ── Double-confirm for destructive reset ──
394
+ if (action === 'reset') {
395
+ p.note(
396
+ 'This will overwrite ALL hooks, skills, agents, and workflow config\n' +
397
+ 'with the default templates. Modified and pinned files will be replaced.\n' +
398
+ 'Your scopes, database, and orbital.config.json are preserved.',
399
+ 'Warning',
400
+ );
401
+ const confirmReset = await p.confirm({
402
+ message: 'Are you sure you want to reset all templates?',
403
+ initialValue: false,
404
+ });
405
+ if (p.isCancel(confirmReset) || !confirmReset) {
406
+ p.cancel('Reset cancelled.');
407
+ process.exit(0);
408
+ }
409
+ const doubleConfirm = await p.confirm({
410
+ message: 'This cannot be undone. Continue?',
411
+ initialValue: false,
412
+ });
413
+ if (p.isCancel(doubleConfirm) || !doubleConfirm) {
414
+ p.cancel('Reset cancelled.');
415
+ process.exit(0);
416
+ }
417
+ }
418
+
419
+ result.action = action;
420
+ return result;
172
421
  }
173
422
 
174
423
  // ─── Template Version Stamping ─────────────────────────────────
@@ -181,7 +430,9 @@ function stampTemplateVersion(projectRoot: string, packageVersion: string): void
181
430
  const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
182
431
  if (config.templateVersion !== packageVersion) {
183
432
  config.templateVersion = packageVersion;
184
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
433
+ const tmp = configPath + `.tmp.${process.pid}`;
434
+ fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + '\n', 'utf8');
435
+ fs.renameSync(tmp, configPath);
185
436
  }
186
437
  } catch { /* ignore malformed config */ }
187
438
  }
@@ -18,8 +18,7 @@ export async function phaseWelcome(state: ProjectSetupState): Promise<boolean> {
18
18
  const action = await p.select({
19
19
  message: 'What would you like to do?',
20
20
  options: [
21
- { value: 'reinit', label: 'Re-initialize (--force)', hint: 'reset all templates to defaults' },
22
- { value: 'configure', label: 'Open config editor', hint: 'modify settings without resetting' },
21
+ { value: 'configure', label: 'Open config editor', hint: 'modify settings' },
23
22
  { value: 'cancel', label: 'Cancel' },
24
23
  ],
25
24
  });
@@ -33,9 +32,6 @@ export async function phaseWelcome(state: ProjectSetupState): Promise<boolean> {
33
32
  await runConfigEditor(state.projectRoot, state.packageVersion, []);
34
33
  process.exit(0);
35
34
  }
36
-
37
- // Re-init — continue through the full project setup with force
38
- return true;
39
35
  }
40
36
 
41
37
  // Not initialized — continue normally
@@ -19,16 +19,15 @@ ${pc.cyan('.claude/')} directory — no database or external service required.`,
19
19
 
20
20
  setupComplete: `${pc.bold('Setup complete.')}
21
21
 
22
- ${pc.cyan('orbital init')} Add a project (run in a project directory)
23
- ${pc.cyan('orbital launch --open')} Start the dashboard
22
+ ${pc.cyan('orbital')} Add a project or launch the dashboard
24
23
  ${pc.cyan('orbital doctor')} Health check & version info`,
25
24
 
26
- addProject: `You can add projects now or later with ${pc.cyan('orbital init')}.
25
+ addProject: `You can add projects now or later by running ${pc.cyan('orbital')} in a project directory.
27
26
  Each project gets its own workflow, scopes, and quality gates.`,
28
27
 
29
28
  // Phase 2: Project setup (runs per-project)
30
29
  reconfigure: `This project is already initialized with Orbital Command.
31
- You can reconfigure settings or run ${pc.cyan('orbital init --force')} to reset.`,
30
+ You can reconfigure settings or select ${pc.cyan('Reset to defaults')} from the hub menu.`,
32
31
 
33
32
  projectConfig: `${pc.bold('Project Config')} ${pc.dim('(.claude/orbital.config.json)')}
34
33
 
@@ -56,16 +55,16 @@ ${pc.cyan('Quality Gates')} Automated checks (lint, typecheck, tests) before tr
56
55
 
57
56
  nextSteps: `${pc.bold('Next Steps')}
58
57
 
59
- 1. ${pc.cyan('orbital launch --open')} Open the dashboard
58
+ 1. Run ${pc.cyan('orbital')} and select ${pc.bold('Launch dashboard')}
60
59
  2. Create a scope from the board or use ${pc.cyan('/scope-create')}
61
60
  3. Use ${pc.cyan('/scope-implement')} to start working on a scope
62
61
 
63
62
  ${pc.bold('Useful Commands')}
64
63
 
64
+ ${pc.cyan('orbital')} Hub menu — launch, config, doctor, etc.
65
65
  ${pc.cyan('orbital status')} See template sync status
66
66
  ${pc.cyan('orbital config')} Modify project settings
67
- ${pc.cyan('orbital update')} Sync to latest templates
68
- ${pc.cyan('orbital doctor')} Health check & version info`,
67
+ ${pc.cyan('orbital update')} Sync to latest templates`,
69
68
  };
70
69
 
71
70
  // ─── Formatting Helpers ─────────────────────────────────────────
@@ -18,3 +18,109 @@ export interface AgentConfig {
18
18
  emoji: string;
19
19
  color: string;
20
20
  }
21
+
22
+ // ─── Dispatch Flags (CLI flags passed to `claude`) ──────────
23
+
24
+ export interface DispatchFlags {
25
+ permissionMode: 'bypass' | 'default' | 'plan' | 'acceptEdits';
26
+ verbose: boolean;
27
+ allowedTools: string[];
28
+ disallowedTools: string[];
29
+ appendSystemPrompt: string;
30
+ outputFormat: '' | 'text' | 'json' | 'stream-json';
31
+ noMarkdown: boolean;
32
+ printMode: boolean;
33
+ }
34
+
35
+ export const DEFAULT_DISPATCH_FLAGS: DispatchFlags = {
36
+ permissionMode: 'bypass',
37
+ verbose: true,
38
+ allowedTools: [],
39
+ disallowedTools: [],
40
+ appendSystemPrompt: '',
41
+ outputFormat: '',
42
+ noMarkdown: false,
43
+ printMode: false,
44
+ };
45
+
46
+ // ─── Dispatch Config (Orbital operational settings) ─────────
47
+
48
+ export interface DispatchConfig {
49
+ staleTimeoutMinutes: number;
50
+ maxBatchSize: number;
51
+ maxConcurrent: number;
52
+ envVars: Record<string, string>;
53
+ }
54
+
55
+ export const DEFAULT_DISPATCH_CONFIG: DispatchConfig = {
56
+ staleTimeoutMinutes: 10,
57
+ maxBatchSize: 20,
58
+ maxConcurrent: 0,
59
+ envVars: {},
60
+ };
61
+
62
+ // ─── Validation ─────────────────────────────────────────────
63
+
64
+ export const VALID_PERMISSION_MODES = ['bypass', 'default', 'plan', 'acceptEdits'] as const;
65
+ export const VALID_OUTPUT_FORMATS = ['', 'text', 'json', 'stream-json'];
66
+ export const VALID_TERMINAL_ADAPTERS = ['auto', 'iterm2', 'subprocess', 'none'];
67
+
68
+ const SAFE_TOOL_NAME = /^[a-zA-Z0-9_:.-]+$/;
69
+ const SAFE_ENV_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
70
+
71
+ export function validateToolName(name: string): boolean {
72
+ return SAFE_TOOL_NAME.test(name);
73
+ }
74
+
75
+ export function validateEnvKey(key: string): boolean {
76
+ return SAFE_ENV_KEY.test(key);
77
+ }
78
+
79
+ export function validateDispatchFlags(flags: Partial<DispatchFlags>): string | null {
80
+ if (flags.permissionMode !== undefined && !VALID_PERMISSION_MODES.includes(flags.permissionMode as typeof VALID_PERMISSION_MODES[number])) {
81
+ return `Invalid permissionMode: ${flags.permissionMode}`;
82
+ }
83
+ if (flags.outputFormat !== undefined && !VALID_OUTPUT_FORMATS.includes(flags.outputFormat as string)) {
84
+ return `Invalid outputFormat: ${flags.outputFormat}`;
85
+ }
86
+ if (flags.allowedTools !== undefined) {
87
+ if (!Array.isArray(flags.allowedTools)) return 'allowedTools must be an array';
88
+ for (const t of flags.allowedTools) {
89
+ if (typeof t !== 'string' || !validateToolName(t)) return `Invalid tool name: ${t}`;
90
+ }
91
+ }
92
+ if (flags.disallowedTools !== undefined) {
93
+ if (!Array.isArray(flags.disallowedTools)) return 'disallowedTools must be an array';
94
+ for (const t of flags.disallowedTools) {
95
+ if (typeof t !== 'string' || !validateToolName(t)) return `Invalid tool name: ${t}`;
96
+ }
97
+ }
98
+ if (flags.appendSystemPrompt !== undefined && typeof flags.appendSystemPrompt !== 'string') {
99
+ return 'appendSystemPrompt must be a string';
100
+ }
101
+ return null;
102
+ }
103
+
104
+ export function validateDispatchConfig(config: Partial<DispatchConfig> & { terminalAdapter?: string }): string | null {
105
+ if (config.terminalAdapter !== undefined && !VALID_TERMINAL_ADAPTERS.includes(config.terminalAdapter)) {
106
+ return `Invalid terminalAdapter: ${config.terminalAdapter}`;
107
+ }
108
+ if (config.staleTimeoutMinutes !== undefined && (typeof config.staleTimeoutMinutes !== 'number' || config.staleTimeoutMinutes < 1)) {
109
+ return 'staleTimeoutMinutes must be a positive number';
110
+ }
111
+ if (config.maxBatchSize !== undefined && (typeof config.maxBatchSize !== 'number' || config.maxBatchSize < 1)) {
112
+ return 'maxBatchSize must be a positive number';
113
+ }
114
+ if (config.maxConcurrent !== undefined && (typeof config.maxConcurrent !== 'number' || config.maxConcurrent < 0)) {
115
+ return 'maxConcurrent must be a non-negative number';
116
+ }
117
+ if (config.envVars !== undefined) {
118
+ if (typeof config.envVars !== 'object' || config.envVars === null || Array.isArray(config.envVars)) {
119
+ return 'envVars must be an object';
120
+ }
121
+ for (const key of Object.keys(config.envVars)) {
122
+ if (!validateEnvKey(key)) return `Invalid env var key: ${key}`;
123
+ }
124
+ }
125
+ return null;
126
+ }
@@ -385,7 +385,7 @@ export class WorkflowEngine {
385
385
  if (!targetList) continue;
386
386
  // Generate aliases for deployment-group targets (dev, staging, production)
387
387
  const group = targetList.group;
388
- if (group === 'deployment') {
388
+ if (group?.startsWith('deploy')) {
389
389
  const sessionKey = targetList.sessionKey ?? '';
390
390
  lines.push(` "to-${edge.to}:${edge.from}:${edge.to}:${sessionKey}"`);
391
391
  }
@@ -90,6 +90,7 @@ Before committing:
90
90
  | Chaos | "What breaks when things go wrong?" |
91
91
  | Frontend Designer | "What does the user see/experience?" |
92
92
  | Architect | "Does this fit our patterns?" |
93
+ | Deep Dive | "What's the full picture before we change anything?" |
93
94
  | Rules Enforcer | "Do all project rules pass?" |
94
95
 
95
96
  ---
@@ -86,6 +86,7 @@ Every enforcement rule exists because a past mistake taught us it matters. Bypas
86
86
  │ "Protect quality and standards" │
87
87
  │ │
88
88
  │ 🏗️ Architect - Patterns, structure, module boundaries │
89
+ │ 🔬 Deep Dive - Codebase audits, refactors, health reviews │
89
90
  │ 📋 Rules Enforcer - Non-negotiable project rules (automated) │
90
91
  │ │
91
92
  └─────────────────────────────────────────────────────────────────────────────┘
@@ -85,6 +85,17 @@ These are **automatic** suggestions based on session state, not user phrases.
85
85
 
86
86
  ---
87
87
 
88
+ ## Agent Auto-Triggers by Phrase
89
+
90
+ | User Says | Agent | Why |
91
+ |-----------|-------|-----|
92
+ | "Refactor", "clean up", "simplify" | 🔬 Deep Dive | Codebase health review needed |
93
+ | "Tech debt", "health check", "audit" | 🔬 Deep Dive | Structural assessment |
94
+ | "Pre-launch review", "before we release" | 🔬 Deep Dive | Comprehensive pre-release audit |
95
+ | "Deep dive", "thorough review" | 🔬 Deep Dive | Explicit invocation |
96
+
97
+ ---
98
+
88
99
  ## Priority Order
89
100
 
90
101
  When multiple skills could apply, prioritize: