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.
- package/README.md +67 -42
- package/bin/commands/config.js +19 -0
- package/bin/commands/events.js +40 -0
- package/bin/commands/launch.js +126 -0
- package/bin/commands/manifest.js +283 -0
- package/bin/commands/registry.js +104 -0
- package/bin/commands/update.js +24 -0
- package/bin/lib/helpers.js +229 -0
- package/bin/orbital.js +95 -870
- package/dist/assets/Landing-CfQdHR0N.js +11 -0
- package/dist/assets/PrimitivesConfig-DThSipFy.js +32 -0
- package/dist/assets/QualityGates-B4kxM5UU.js +26 -0
- package/dist/assets/SessionTimeline-Bz1iZnmg.js +1 -0
- package/dist/assets/Settings-DLcZwbCT.js +12 -0
- package/dist/assets/SourceControl-BMNIz7Lt.js +36 -0
- package/dist/assets/WorkflowVisualizer-CxuSBOYu.js +69 -0
- package/dist/assets/{arrow-down-CPy85_J6.js → arrow-down-DVPp6_qp.js} +1 -1
- package/dist/assets/bot-NFaJBDn_.js +6 -0
- package/dist/assets/{charts-DbDg0Psc.js → charts-LGLb8hyU.js} +1 -1
- package/dist/assets/{circle-x-Cwz6ZQDV.js → circle-x-IsFCkBZu.js} +1 -1
- package/dist/assets/{file-text-C46Xr65c.js → file-text-J1cebZXF.js} +1 -1
- package/dist/assets/{globe-Cn2yNZUD.js → globe-WzeyHsUc.js} +1 -1
- package/dist/assets/index-BdJ57EhC.css +1 -0
- package/dist/assets/index-o4ScMAuR.js +349 -0
- package/dist/assets/{key-OPaNTWJ5.js → key-CKR8JJSj.js} +1 -1
- package/dist/assets/{minus-GMsbpKym.js → minus-CHBsJyjp.js} +1 -1
- package/dist/assets/radio-xqZaR-Uk.js +6 -0
- package/dist/assets/rocket-D_xvvNG6.js +6 -0
- package/dist/assets/{shield-DwAFkDYI.js → shield-TdB1yv_a.js} +1 -1
- package/dist/assets/useSocketListener-0L5yiN5i.js +1 -0
- package/dist/assets/useWorkflowEditor-CqeRWVQX.js +11 -0
- package/dist/assets/workflow-constants-Rw-GmgHZ.js +6 -0
- package/dist/assets/zap-C9wqYMpl.js +6 -0
- package/dist/index.html +3 -3
- package/dist/server/server/__tests__/data-routes.test.js +2 -0
- package/dist/server/server/__tests__/scope-routes.test.js +1 -0
- package/dist/server/server/config-migrator.js +0 -3
- package/dist/server/server/config.js +35 -6
- package/dist/server/server/database.js +0 -22
- package/dist/server/server/index.js +26 -814
- package/dist/server/server/init.js +32 -399
- package/dist/server/server/launch.js +1 -1
- package/dist/server/server/parsers/event-parser.js +4 -1
- package/dist/server/server/project-context.js +19 -9
- package/dist/server/server/project-manager.js +6 -6
- package/dist/server/server/routes/aggregate-routes.js +871 -0
- package/dist/server/server/routes/config-routes.js +41 -88
- package/dist/server/server/routes/data-routes.js +5 -15
- package/dist/server/server/routes/dispatch-routes.js +24 -8
- package/dist/server/server/routes/manifest-routes.js +1 -1
- package/dist/server/server/routes/scope-routes.js +10 -7
- package/dist/server/server/schema.js +1 -0
- package/dist/server/server/services/batch-orchestrator.js +17 -3
- package/dist/server/server/services/config-service.js +10 -1
- package/dist/server/server/services/scope-service.js +7 -7
- package/dist/server/server/services/sprint-orchestrator.js +24 -11
- package/dist/server/server/services/sprint-service.js +2 -2
- package/dist/server/server/uninstall.js +195 -0
- package/dist/server/server/update.js +212 -0
- package/dist/server/server/utils/dispatch-utils.js +8 -6
- package/dist/server/server/utils/flag-builder.js +54 -0
- package/dist/server/server/utils/json-fields.js +14 -0
- package/dist/server/server/utils/json-fields.test.js +73 -0
- package/dist/server/server/utils/route-helpers.js +37 -0
- package/dist/server/server/utils/route-helpers.test.js +115 -0
- package/dist/server/server/watchers/event-watcher.js +28 -13
- package/dist/server/server/wizard/config-editor.js +4 -4
- package/dist/server/server/wizard/doctor.js +2 -2
- package/dist/server/server/wizard/index.js +224 -39
- package/dist/server/server/wizard/phases/welcome.js +1 -4
- package/dist/server/server/wizard/ui.js +6 -7
- package/dist/server/shared/api-types.js +80 -1
- package/dist/server/shared/workflow-engine.js +1 -1
- package/package.json +20 -20
- package/schemas/orbital.config.schema.json +1 -19
- package/scripts/postinstall.js +6 -42
- package/scripts/release.sh +53 -0
- package/server/__tests__/data-routes.test.ts +2 -0
- package/server/__tests__/scope-routes.test.ts +1 -0
- package/server/config-migrator.ts +0 -3
- package/server/config.ts +39 -11
- package/server/database.ts +0 -26
- package/server/global-config.ts +4 -0
- package/server/index.ts +29 -894
- package/server/init.ts +32 -443
- package/server/launch.ts +1 -1
- package/server/parsers/event-parser.ts +4 -1
- package/server/project-context.ts +26 -10
- package/server/project-manager.ts +5 -6
- package/server/routes/aggregate-routes.ts +968 -0
- package/server/routes/config-routes.ts +41 -81
- package/server/routes/data-routes.ts +7 -16
- package/server/routes/dispatch-routes.ts +29 -8
- package/server/routes/manifest-routes.ts +1 -1
- package/server/routes/scope-routes.ts +12 -7
- package/server/schema.ts +1 -0
- package/server/services/batch-orchestrator.ts +18 -2
- package/server/services/config-service.ts +10 -1
- package/server/services/scope-service.ts +6 -6
- package/server/services/sprint-orchestrator.ts +24 -9
- package/server/services/sprint-service.ts +2 -2
- package/server/uninstall.ts +214 -0
- package/server/update.ts +263 -0
- package/server/utils/dispatch-utils.ts +8 -6
- package/server/utils/flag-builder.ts +56 -0
- package/server/utils/json-fields.test.ts +83 -0
- package/server/utils/json-fields.ts +14 -0
- package/server/utils/route-helpers.test.ts +144 -0
- package/server/utils/route-helpers.ts +38 -0
- package/server/watchers/event-watcher.ts +24 -12
- package/server/wizard/config-editor.ts +4 -4
- package/server/wizard/doctor.ts +2 -2
- package/server/wizard/index.ts +291 -40
- package/server/wizard/phases/welcome.ts +1 -5
- package/server/wizard/ui.ts +6 -7
- package/shared/api-types.ts +106 -0
- package/shared/workflow-engine.ts +1 -1
- package/templates/agents/QUICK-REFERENCE.md +1 -0
- package/templates/agents/README.md +1 -0
- package/templates/agents/SKILL-TRIGGERS.md +11 -0
- package/templates/agents/green-team/deep-dive.md +361 -0
- package/templates/hooks/end-session.sh +1 -0
- package/templates/hooks/init-session.sh +1 -0
- package/templates/hooks/scope-commit-logger.sh +2 -2
- package/templates/hooks/scope-create-gate.sh +2 -4
- package/templates/hooks/scope-gate.sh +4 -6
- package/templates/hooks/scope-helpers.sh +10 -1
- package/templates/hooks/scope-lifecycle-gate.sh +14 -5
- package/templates/hooks/scope-prepare.sh +1 -1
- package/templates/hooks/scope-transition.sh +14 -6
- package/templates/hooks/time-tracker.sh +2 -5
- package/templates/orbital.config.json +1 -4
- package/templates/presets/development.json +4 -4
- package/templates/presets/gitflow.json +7 -0
- package/templates/prompts/README.md +23 -0
- package/templates/prompts/deep-dive-audit.md +94 -0
- package/templates/quick/rules.md +56 -5
- package/templates/skills/git-commit/SKILL.md +21 -6
- package/templates/skills/git-dev/SKILL.md +8 -4
- package/templates/skills/git-main/SKILL.md +8 -4
- package/templates/skills/git-production/SKILL.md +6 -3
- package/templates/skills/git-staging/SKILL.md +6 -3
- package/templates/skills/scope-fix-review/SKILL.md +8 -4
- package/templates/skills/scope-implement/SKILL.md +13 -5
- package/templates/skills/scope-post-review/SKILL.md +16 -4
- package/templates/skills/scope-pre-review/SKILL.md +6 -2
- package/dist/assets/PrimitivesConfig-CrmQXYh4.js +0 -32
- package/dist/assets/QualityGates-BbasOsF3.js +0 -21
- package/dist/assets/SessionTimeline-CGeJsVvy.js +0 -1
- package/dist/assets/Settings-oiM496mc.js +0 -12
- package/dist/assets/SourceControl-B1fP2nJL.js +0 -41
- package/dist/assets/WorkflowVisualizer-CWLYf-f0.js +0 -74
- package/dist/assets/formatDistanceToNow-BMqsSP44.js +0 -1
- package/dist/assets/index-Aj4sV8Al.css +0 -1
- package/dist/assets/index-Bc9dK3MW.js +0 -354
- package/dist/assets/useWorkflowEditor-BJkTX_NR.js +0 -16
- package/dist/assets/zap-DfbUoOty.js +0 -11
- package/dist/server/server/services/telemetry-service.js +0 -143
- package/server/services/telemetry-service.ts +0 -195
- /package/{shared/default-workflow.json → templates/presets/default.json} +0 -0
|
@@ -3,7 +3,7 @@ import type { Emitter } from '../project-emitter.js';
|
|
|
3
3
|
import { ConfigService, isValidPrimitiveType } from '../services/config-service.js';
|
|
4
4
|
import type { ConfigPrimitiveType } from '../services/config-service.js';
|
|
5
5
|
import type { WorkflowService } from '../services/workflow-service.js';
|
|
6
|
-
import {
|
|
6
|
+
import { catchRoute, inferErrorStatus } from '../utils/route-helpers.js';
|
|
7
7
|
|
|
8
8
|
interface ConfigRouteDeps {
|
|
9
9
|
projectRoot: string;
|
|
@@ -25,21 +25,17 @@ export function createConfigRoutes({ projectRoot, workflowService: _workflowServ
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// GET /config/:type/tree — directory tree with frontmatter
|
|
28
|
-
router.get('/config/:type/tree', (req, res) => {
|
|
28
|
+
router.get('/config/:type/tree', catchRoute((req, res) => {
|
|
29
29
|
const type = parseType(req.params.type, res);
|
|
30
30
|
if (!type) return;
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
} catch (err) {
|
|
37
|
-
res.status(500).json({ success: false, error: errMsg(err) });
|
|
38
|
-
}
|
|
39
|
-
});
|
|
32
|
+
const basePath = configService.getBasePath(type);
|
|
33
|
+
const tree = configService.scanDirectory(basePath);
|
|
34
|
+
res.json({ success: true, data: tree });
|
|
35
|
+
}));
|
|
40
36
|
|
|
41
37
|
// GET /config/:type/file?path=<relative> — file content
|
|
42
|
-
router.get('/config/:type/file', (req, res) => {
|
|
38
|
+
router.get('/config/:type/file', catchRoute((req, res) => {
|
|
43
39
|
const type = parseType(req.params.type, res);
|
|
44
40
|
if (!type) return;
|
|
45
41
|
|
|
@@ -49,19 +45,13 @@ export function createConfigRoutes({ projectRoot, workflowService: _workflowServ
|
|
|
49
45
|
return;
|
|
50
46
|
}
|
|
51
47
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
} catch (err) {
|
|
57
|
-
const msg = errMsg(err);
|
|
58
|
-
const status = msg.includes('traversal') ? 403 : msg.includes('ENOENT') || msg.includes('not found') ? 404 : 500;
|
|
59
|
-
res.status(status).json({ success: false, error: msg });
|
|
60
|
-
}
|
|
61
|
-
});
|
|
48
|
+
const basePath = configService.getBasePath(type);
|
|
49
|
+
const content = configService.readFile(basePath, filePath);
|
|
50
|
+
res.json({ success: true, data: { path: filePath, content } });
|
|
51
|
+
}, inferErrorStatus));
|
|
62
52
|
|
|
63
53
|
// PUT /config/:type/file — save file { path, content }
|
|
64
|
-
router.put('/config/:type/file', (req, res) => {
|
|
54
|
+
router.put('/config/:type/file', catchRoute((req, res) => {
|
|
65
55
|
const type = parseType(req.params.type, res);
|
|
66
56
|
if (!type) return;
|
|
67
57
|
|
|
@@ -71,20 +61,14 @@ export function createConfigRoutes({ projectRoot, workflowService: _workflowServ
|
|
|
71
61
|
return;
|
|
72
62
|
}
|
|
73
63
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
} catch (err) {
|
|
80
|
-
const msg = errMsg(err);
|
|
81
|
-
const status = msg.includes('traversal') ? 403 : msg.includes('not found') ? 404 : 500;
|
|
82
|
-
res.status(status).json({ success: false, error: msg });
|
|
83
|
-
}
|
|
84
|
-
});
|
|
64
|
+
const basePath = configService.getBasePath(type);
|
|
65
|
+
configService.writeFile(basePath, filePath, content);
|
|
66
|
+
io.emit(`config:${type}:changed`, { action: 'updated', path: filePath });
|
|
67
|
+
res.json({ success: true });
|
|
68
|
+
}, inferErrorStatus));
|
|
85
69
|
|
|
86
70
|
// POST /config/:type/file — create file { path, content }
|
|
87
|
-
router.post('/config/:type/file', (req, res) => {
|
|
71
|
+
router.post('/config/:type/file', catchRoute((req, res) => {
|
|
88
72
|
const type = parseType(req.params.type, res);
|
|
89
73
|
if (!type) return;
|
|
90
74
|
|
|
@@ -94,20 +78,14 @@ export function createConfigRoutes({ projectRoot, workflowService: _workflowServ
|
|
|
94
78
|
return;
|
|
95
79
|
}
|
|
96
80
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
} catch (err) {
|
|
103
|
-
const msg = errMsg(err);
|
|
104
|
-
const status = msg.includes('traversal') ? 403 : msg.includes('already exists') ? 409 : 500;
|
|
105
|
-
res.status(status).json({ success: false, error: msg });
|
|
106
|
-
}
|
|
107
|
-
});
|
|
81
|
+
const basePath = configService.getBasePath(type);
|
|
82
|
+
configService.createFile(basePath, filePath, content);
|
|
83
|
+
io.emit(`config:${type}:changed`, { action: 'created', path: filePath });
|
|
84
|
+
res.status(201).json({ success: true });
|
|
85
|
+
}, inferErrorStatus));
|
|
108
86
|
|
|
109
87
|
// DELETE /config/:type/file?path=<relative> — delete file
|
|
110
|
-
router.delete('/config/:type/file', (req, res) => {
|
|
88
|
+
router.delete('/config/:type/file', catchRoute((req, res) => {
|
|
111
89
|
const type = parseType(req.params.type, res);
|
|
112
90
|
if (!type) return;
|
|
113
91
|
|
|
@@ -117,20 +95,14 @@ export function createConfigRoutes({ projectRoot, workflowService: _workflowServ
|
|
|
117
95
|
return;
|
|
118
96
|
}
|
|
119
97
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
} catch (err) {
|
|
126
|
-
const msg = errMsg(err);
|
|
127
|
-
const status = msg.includes('traversal') ? 403 : msg.includes('not found') ? 404 : msg.includes('directory') ? 400 : 500;
|
|
128
|
-
res.status(status).json({ success: false, error: msg });
|
|
129
|
-
}
|
|
130
|
-
});
|
|
98
|
+
const basePath = configService.getBasePath(type);
|
|
99
|
+
configService.deleteFile(basePath, filePath);
|
|
100
|
+
io.emit(`config:${type}:changed`, { action: 'deleted', path: filePath });
|
|
101
|
+
res.json({ success: true });
|
|
102
|
+
}, inferErrorStatus));
|
|
131
103
|
|
|
132
104
|
// POST /config/:type/rename — rename { oldPath, newPath }
|
|
133
|
-
router.post('/config/:type/rename', (req, res) => {
|
|
105
|
+
router.post('/config/:type/rename', catchRoute((req, res) => {
|
|
134
106
|
const type = parseType(req.params.type, res);
|
|
135
107
|
if (!type) return;
|
|
136
108
|
|
|
@@ -140,20 +112,14 @@ export function createConfigRoutes({ projectRoot, workflowService: _workflowServ
|
|
|
140
112
|
return;
|
|
141
113
|
}
|
|
142
114
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
} catch (err) {
|
|
149
|
-
const msg = errMsg(err);
|
|
150
|
-
const status = msg.includes('traversal') ? 403 : msg.includes('not found') ? 404 : msg.includes('already exists') ? 409 : 500;
|
|
151
|
-
res.status(status).json({ success: false, error: msg });
|
|
152
|
-
}
|
|
153
|
-
});
|
|
115
|
+
const basePath = configService.getBasePath(type);
|
|
116
|
+
configService.renameFile(basePath, oldPath, newPath);
|
|
117
|
+
io.emit(`config:${type}:changed`, { action: 'renamed', oldPath, newPath });
|
|
118
|
+
res.json({ success: true });
|
|
119
|
+
}, inferErrorStatus));
|
|
154
120
|
|
|
155
121
|
// POST /config/:type/folder — create folder { path }
|
|
156
|
-
router.post('/config/:type/folder', (req, res) => {
|
|
122
|
+
router.post('/config/:type/folder', catchRoute((req, res) => {
|
|
157
123
|
const type = parseType(req.params.type, res);
|
|
158
124
|
if (!type) return;
|
|
159
125
|
|
|
@@ -163,17 +129,11 @@ export function createConfigRoutes({ projectRoot, workflowService: _workflowServ
|
|
|
163
129
|
return;
|
|
164
130
|
}
|
|
165
131
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
} catch (err) {
|
|
172
|
-
const msg = errMsg(err);
|
|
173
|
-
const status = msg.includes('traversal') ? 403 : msg.includes('already exists') ? 409 : 500;
|
|
174
|
-
res.status(status).json({ success: false, error: msg });
|
|
175
|
-
}
|
|
176
|
-
});
|
|
132
|
+
const basePath = configService.getBasePath(type);
|
|
133
|
+
configService.createFolder(basePath, folderPath);
|
|
134
|
+
io.emit(`config:${type}:changed`, { action: 'folder-created', path: folderPath });
|
|
135
|
+
res.status(201).json({ success: true });
|
|
136
|
+
}, inferErrorStatus));
|
|
177
137
|
|
|
178
138
|
return router;
|
|
179
139
|
}
|
|
@@ -12,26 +12,15 @@ import type { WorkflowEngine } from '../../shared/workflow-engine.js';
|
|
|
12
12
|
import { getHookEnforcement } from '../../shared/workflow-config.js';
|
|
13
13
|
import { getClaudeSessions, getSessionStats, type SessionStats } from '../services/claude-session-service.js';
|
|
14
14
|
import { launchInTerminal } from '../utils/terminal-launcher.js';
|
|
15
|
+
import type { OrbitalConfig } from '../config.js';
|
|
16
|
+
import { buildClaudeFlags } from '../utils/flag-builder.js';
|
|
15
17
|
import { createLogger } from '../utils/logger.js';
|
|
18
|
+
import { parseJsonFields, type Row } from '../utils/json-fields.js';
|
|
16
19
|
|
|
17
20
|
const log = createLogger('server');
|
|
18
21
|
|
|
19
22
|
const execFileAsync = promisify(execFile);
|
|
20
23
|
|
|
21
|
-
const JSON_FIELDS = ['tags', 'blocked_by', 'blocks', 'data', 'discoveries', 'next_steps', 'details'];
|
|
22
|
-
|
|
23
|
-
type Row = Record<string, unknown>;
|
|
24
|
-
|
|
25
|
-
function parseJsonFields(row: Row): Row {
|
|
26
|
-
const parsed = { ...row };
|
|
27
|
-
for (const field of JSON_FIELDS) {
|
|
28
|
-
if (typeof parsed[field] === 'string') {
|
|
29
|
-
try { parsed[field] = JSON.parse(parsed[field] as string); } catch { /* keep string */ }
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return parsed;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
24
|
// ─── Route Factory ──────────────────────────────────────────
|
|
36
25
|
|
|
37
26
|
interface DataRouteDeps {
|
|
@@ -44,10 +33,11 @@ interface DataRouteDeps {
|
|
|
44
33
|
engine: WorkflowEngine;
|
|
45
34
|
projectRoot: string;
|
|
46
35
|
inferScopeStatus: (type: string, scopeId: unknown, data: Record<string, unknown>) => void;
|
|
36
|
+
config: OrbitalConfig;
|
|
47
37
|
}
|
|
48
38
|
|
|
49
39
|
export function createDataRoutes({
|
|
50
|
-
db, io, eventService, gateService, deployService, gitService, engine, projectRoot, inferScopeStatus,
|
|
40
|
+
db, io, eventService, gateService, deployService, gitService, engine, projectRoot, inferScopeStatus, config,
|
|
51
41
|
}: DataRouteDeps): Router {
|
|
52
42
|
const router = Router();
|
|
53
43
|
|
|
@@ -353,7 +343,8 @@ export function createDataRoutes({
|
|
|
353
343
|
return;
|
|
354
344
|
}
|
|
355
345
|
|
|
356
|
-
const
|
|
346
|
+
const flagsStr = buildClaudeFlags(config.claude.dispatchFlags);
|
|
347
|
+
const resumeCmd = `cd '${projectRoot}' && claude ${flagsStr} --resume '${claude_session_id}'`;
|
|
357
348
|
|
|
358
349
|
try {
|
|
359
350
|
await launchInTerminal(resumeCmd);
|
|
@@ -5,11 +5,13 @@ import type { ScopeService } from '../services/scope-service.js';
|
|
|
5
5
|
import { launchInCategorizedTerminal, escapeForAnsiC, shellQuote, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
|
|
6
6
|
import { resolveDispatchEvent, resolveAbandonedDispatchesForScope, getActiveScopeIds, getAbandonedScopeIds, linkPidToDispatch } from '../utils/dispatch-utils.js';
|
|
7
7
|
import type { WorkflowEngine } from '../../shared/workflow-engine.js';
|
|
8
|
+
import type { OrbitalConfig } from '../config.js';
|
|
9
|
+
import { buildClaudeFlags, buildEnvVarPrefix } from '../utils/flag-builder.js';
|
|
8
10
|
import { createLogger } from '../utils/logger.js';
|
|
9
11
|
|
|
10
12
|
const log = createLogger('dispatch');
|
|
11
13
|
|
|
12
|
-
const
|
|
14
|
+
const DEFAULT_MAX_BATCH_SIZE = 20;
|
|
13
15
|
|
|
14
16
|
interface DispatchBody {
|
|
15
17
|
scope_id?: number;
|
|
@@ -24,9 +26,10 @@ interface DispatchRouteDeps {
|
|
|
24
26
|
scopeService: ScopeService;
|
|
25
27
|
projectRoot: string;
|
|
26
28
|
engine: WorkflowEngine;
|
|
29
|
+
config: OrbitalConfig;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
export function createDispatchRoutes({ db, io, scopeService, projectRoot, engine }: DispatchRouteDeps): Router {
|
|
32
|
+
export function createDispatchRoutes({ db, io, scopeService, projectRoot, engine, config }: DispatchRouteDeps): Router {
|
|
30
33
|
const router = Router();
|
|
31
34
|
|
|
32
35
|
router.get('/dispatch/active-scopes', (_req, res) => {
|
|
@@ -79,6 +82,19 @@ export function createDispatchRoutes({ db, io, scopeService, projectRoot, engine
|
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
// Max concurrent dispatches guard
|
|
86
|
+
const maxConcurrent = config.dispatch.maxConcurrent;
|
|
87
|
+
if (maxConcurrent > 0) {
|
|
88
|
+
const activeCount = (db.prepare(
|
|
89
|
+
`SELECT COUNT(*) as count FROM events
|
|
90
|
+
WHERE type = 'DISPATCH' AND JSON_EXTRACT(data, '$.resolved') IS NULL`
|
|
91
|
+
).get() as { count: number }).count;
|
|
92
|
+
if (activeCount >= maxConcurrent) {
|
|
93
|
+
res.status(429).json({ error: `Max concurrent dispatches reached (${maxConcurrent})` });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
82
98
|
// Update scope status if transition provided
|
|
83
99
|
if (scope_id != null && transition?.to) {
|
|
84
100
|
const result = scopeService.updateStatus(scope_id, transition.to, 'dispatch');
|
|
@@ -107,10 +123,12 @@ export function createDispatchRoutes({ db, io, scopeService, projectRoot, engine
|
|
|
107
123
|
const sessionName = buildSessionName({ scopeId: scope_id ?? undefined, title: scope?.title, command });
|
|
108
124
|
const beforePids = snapshotSessionPids(projectRoot);
|
|
109
125
|
|
|
110
|
-
// Launch in iTerm — interactive TUI mode (no -p) for full visibility
|
|
126
|
+
// Launch in iTerm — interactive TUI mode (no -p unless printMode) for full visibility
|
|
111
127
|
const promptText = prompt ?? command;
|
|
112
128
|
const escaped = escapeForAnsiC(promptText);
|
|
113
|
-
const
|
|
129
|
+
const flagsStr = buildClaudeFlags(config.claude.dispatchFlags);
|
|
130
|
+
const envPrefix = buildEnvVarPrefix(config.dispatch.envVars);
|
|
131
|
+
const fullCmd = `cd '${shellQuote(projectRoot)}' && ${envPrefix}ORBITAL_DISPATCH_ID='${shellQuote(eventId)}' claude ${flagsStr} $'${escaped}'`;
|
|
114
132
|
try {
|
|
115
133
|
await launchInCategorizedTerminal(command, fullCmd, sessionName);
|
|
116
134
|
res.json({ ok: true, dispatch_id: eventId, scope_id: scope_id ?? null });
|
|
@@ -215,8 +233,9 @@ export function createDispatchRoutes({ db, io, scopeService, projectRoot, engine
|
|
|
215
233
|
}
|
|
216
234
|
|
|
217
235
|
// W-12: Validate batch size and scope ID types
|
|
218
|
-
|
|
219
|
-
|
|
236
|
+
const maxBatch = config.dispatch.maxBatchSize || DEFAULT_MAX_BATCH_SIZE;
|
|
237
|
+
if (scope_ids.length > maxBatch) {
|
|
238
|
+
res.status(400).json({ error: `Maximum batch size is ${maxBatch}` });
|
|
220
239
|
return;
|
|
221
240
|
}
|
|
222
241
|
if (!scope_ids.every(id => Number.isInteger(id) && id > 0)) {
|
|
@@ -249,10 +268,12 @@ export function createDispatchRoutes({ db, io, scopeService, projectRoot, engine
|
|
|
249
268
|
timestamp: new Date().toISOString(),
|
|
250
269
|
});
|
|
251
270
|
|
|
252
|
-
// Launch single CLI session
|
|
271
|
+
// Launch single CLI session with batch env vars
|
|
253
272
|
const batchEscaped = escapeForAnsiC(command);
|
|
254
273
|
const beforePids = snapshotSessionPids(projectRoot);
|
|
255
|
-
const
|
|
274
|
+
const batchFlags = buildClaudeFlags(config.claude.dispatchFlags);
|
|
275
|
+
const envPrefix = buildEnvVarPrefix(config.dispatch.envVars);
|
|
276
|
+
const fullCmd = `cd '${shellQuote(projectRoot)}' && ${envPrefix}ORBITAL_DISPATCH_ID='${shellQuote(eventId)}' claude ${batchFlags} $'${batchEscaped}'`;
|
|
256
277
|
try {
|
|
257
278
|
await launchInCategorizedTerminal(command, fullCmd);
|
|
258
279
|
res.json({ ok: true, dispatch_id: eventId, scope_ids });
|
|
@@ -166,7 +166,7 @@ export function createManifestRoutes({
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
if (!manifest) {
|
|
169
|
-
return res.status(400).json({ success: false, error: 'No manifest. Run orbital
|
|
169
|
+
return res.status(400).json({ success: false, error: 'No manifest. Run orbital first.' });
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
if (dryRun) {
|
|
@@ -5,8 +5,10 @@ import type { Emitter } from '../project-emitter.js';
|
|
|
5
5
|
import type { ScopeService } from '../services/scope-service.js';
|
|
6
6
|
import type { ReadinessService } from '../services/readiness-service.js';
|
|
7
7
|
import type { WorkflowEngine } from '../../shared/workflow-engine.js';
|
|
8
|
-
import { launchInTerminal, escapeForAnsiC, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
|
|
8
|
+
import { launchInTerminal, escapeForAnsiC, shellQuote, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
|
|
9
9
|
import { resolveDispatchEvent, linkPidToDispatch } from '../utils/dispatch-utils.js';
|
|
10
|
+
import { buildClaudeFlags, buildEnvVarPrefix } from '../utils/flag-builder.js';
|
|
11
|
+
import type { OrbitalConfig } from '../config.js';
|
|
10
12
|
import { createLogger } from '../utils/logger.js';
|
|
11
13
|
|
|
12
14
|
const log = createLogger('dispatch');
|
|
@@ -19,13 +21,14 @@ interface ScopeRouteDeps {
|
|
|
19
21
|
projectRoot: string;
|
|
20
22
|
projectName: string;
|
|
21
23
|
engine: WorkflowEngine;
|
|
24
|
+
config: OrbitalConfig;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
function isValidSlug(slug: string): boolean {
|
|
25
28
|
return /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(slug) && slug.length <= 80;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
export function createScopeRoutes({ db, io, scopeService, readinessService, projectRoot, projectName, engine }: ScopeRouteDeps): Router {
|
|
31
|
+
export function createScopeRoutes({ db, io, scopeService, readinessService, projectRoot, projectName, engine, config }: ScopeRouteDeps): Router {
|
|
29
32
|
const router = Router();
|
|
30
33
|
|
|
31
34
|
// ─── Scope CRUD ──────────────────────────────────────────
|
|
@@ -123,7 +126,10 @@ export function createScopeRoutes({ db, io, scopeService, readinessService, proj
|
|
|
123
126
|
router.post('/ideas/:slug/promote', async (req, res) => {
|
|
124
127
|
const { slug } = req.params;
|
|
125
128
|
if (!isValidSlug(slug)) { res.status(400).json({ error: 'Invalid slug' }); return; }
|
|
126
|
-
const
|
|
129
|
+
const entryPoint = engine.getEntryPoint();
|
|
130
|
+
const targets = engine.getValidTargets(entryPoint.id);
|
|
131
|
+
const promoteTarget = targets[0] ?? 'planning';
|
|
132
|
+
const result = scopeService.promoteIdea(slug, promoteTarget);
|
|
127
133
|
if (!result) {
|
|
128
134
|
res.status(404).json({ error: 'Idea not found' });
|
|
129
135
|
return;
|
|
@@ -132,9 +138,6 @@ export function createScopeRoutes({ db, io, scopeService, readinessService, proj
|
|
|
132
138
|
const scopeId = result.id;
|
|
133
139
|
|
|
134
140
|
// Read command from workflow edge config (user-overridable)
|
|
135
|
-
const entryPoint = engine.getEntryPoint();
|
|
136
|
-
const targets = engine.getValidTargets(entryPoint.id);
|
|
137
|
-
const promoteTarget = targets[0] ?? 'planning';
|
|
138
141
|
const edge = engine.findEdge(entryPoint.id, promoteTarget);
|
|
139
142
|
const edgeCommand = edge ? engine.buildCommand(edge, scopeId) : null;
|
|
140
143
|
const command = edgeCommand ?? `/scope-create ${String(scopeId).padStart(3, '0')}`;
|
|
@@ -158,7 +161,9 @@ export function createScopeRoutes({ db, io, scopeService, readinessService, proj
|
|
|
158
161
|
});
|
|
159
162
|
|
|
160
163
|
const escaped = escapeForAnsiC(command);
|
|
161
|
-
const
|
|
164
|
+
const flagsStr = buildClaudeFlags(config.claude.dispatchFlags);
|
|
165
|
+
const envPrefix = buildEnvVarPrefix(config.dispatch.envVars);
|
|
166
|
+
const fullCmd = `cd '${shellQuote(projectRoot)}' && ${envPrefix}ORBITAL_DISPATCH_ID='${shellQuote(eventId)}' claude ${flagsStr} $'${escaped}'`;
|
|
162
167
|
|
|
163
168
|
const promoteSessionName = buildSessionName({ scopeId, title: result.title, command });
|
|
164
169
|
const promoteBeforePids = snapshotSessionPids(projectRoot);
|
package/server/schema.ts
CHANGED
|
@@ -89,4 +89,5 @@ CREATE INDEX IF NOT EXISTS idx_sessions_scope ON sessions(scope_id);
|
|
|
89
89
|
CREATE INDEX IF NOT EXISTS idx_sessions_claude_id ON sessions(claude_session_id);
|
|
90
90
|
CREATE INDEX IF NOT EXISTS idx_sprints_status ON sprints(status);
|
|
91
91
|
CREATE INDEX IF NOT EXISTS idx_sprint_scopes_sprint ON sprint_scopes(sprint_id);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_events_dispatch_unresolved ON events(type, scope_id) WHERE type = 'DISPATCH' AND JSON_EXTRACT(data, '$.resolved') IS NULL;
|
|
92
93
|
`;
|
|
@@ -2,9 +2,11 @@ import type Database from 'better-sqlite3';
|
|
|
2
2
|
import type { Emitter } from '../project-emitter.js';
|
|
3
3
|
import type { SprintService } from './sprint-service.js';
|
|
4
4
|
import type { ScopeService } from './scope-service.js';
|
|
5
|
-
import { launchInCategorizedTerminal, escapeForAnsiC, snapshotSessionPids, discoverNewSession, isSessionPidAlive } from '../utils/terminal-launcher.js';
|
|
5
|
+
import { launchInCategorizedTerminal, escapeForAnsiC, shellQuote, snapshotSessionPids, discoverNewSession, isSessionPidAlive } from '../utils/terminal-launcher.js';
|
|
6
6
|
import { linkPidToDispatch, resolveDispatchEvent } from '../utils/dispatch-utils.js';
|
|
7
7
|
import type { WorkflowEngine } from '../../shared/workflow-engine.js';
|
|
8
|
+
import type { OrbitalConfig } from '../config.js';
|
|
9
|
+
import { buildClaudeFlags, buildEnvVarPrefix } from '../utils/flag-builder.js';
|
|
8
10
|
import { createLogger } from '../utils/logger.js';
|
|
9
11
|
|
|
10
12
|
const log = createLogger('batch');
|
|
@@ -20,6 +22,7 @@ export class BatchOrchestrator {
|
|
|
20
22
|
private scopeService: ScopeService,
|
|
21
23
|
private engine: WorkflowEngine,
|
|
22
24
|
private projectRoot: string,
|
|
25
|
+
private config: OrbitalConfig,
|
|
23
26
|
) {}
|
|
24
27
|
|
|
25
28
|
/** Dispatch a batch — validates constraints and routes to column-specific handler */
|
|
@@ -70,7 +73,9 @@ export class BatchOrchestrator {
|
|
|
70
73
|
|
|
71
74
|
// Launch single CLI session with BATCH_SCOPE_IDS prepended to command
|
|
72
75
|
const escaped = escapeForAnsiC(command);
|
|
73
|
-
const
|
|
76
|
+
const flagsStr = buildClaudeFlags(this.config.claude.dispatchFlags);
|
|
77
|
+
const envPrefix = buildEnvVarPrefix(this.config.dispatch.envVars);
|
|
78
|
+
const fullCmd = `cd '${shellQuote(this.projectRoot)}' && ${envPrefix}ORBITAL_DISPATCH_ID='${shellQuote(eventId)}' BATCH_SCOPE_IDS='${scopeIdsStr}' MERGE_MODE='${mergeModeStr}' claude ${flagsStr} $'${escaped}'`;
|
|
74
79
|
const beforePids = snapshotSessionPids(this.projectRoot);
|
|
75
80
|
|
|
76
81
|
try {
|
|
@@ -148,6 +153,17 @@ export class BatchOrchestrator {
|
|
|
148
153
|
if (batch.status !== 'dispatched' && batch.status !== 'in_progress') return;
|
|
149
154
|
|
|
150
155
|
const scopes = this.sprintService.getSprintScopes(batchId);
|
|
156
|
+
|
|
157
|
+
// If batch never reached 'in_progress', the session never started —
|
|
158
|
+
// don't credit any scope regardless of their current workflow status
|
|
159
|
+
if (batch.status === 'dispatched') {
|
|
160
|
+
this.sprintService.updateStatus(batchId, 'failed');
|
|
161
|
+
for (const ss of scopes) {
|
|
162
|
+
this.sprintService.updateScopeStatus(batchId, ss.scope_id, 'failed', 'Session never started');
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
151
167
|
const allTransitioned = scopes.every((ss) => ss.dispatch_status === 'completed');
|
|
152
168
|
|
|
153
169
|
if (allTransitioned) {
|
|
@@ -120,7 +120,16 @@ export class ConfigService {
|
|
|
120
120
|
const fullPath = path.join(currentPath, entry.name);
|
|
121
121
|
const relPath = path.relative(basePath, fullPath);
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
// Resolve symlinks: Dirent.isDirectory() returns false for symlinks-to-dirs.
|
|
124
|
+
// Self-hosted projects symlink .claude/agents/*, .claude/hooks/*, etc. into templates/.
|
|
125
|
+
let stat: fs.Stats;
|
|
126
|
+
try {
|
|
127
|
+
stat = fs.statSync(fullPath);
|
|
128
|
+
} catch {
|
|
129
|
+
continue; // broken symlink — skip silently
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (stat.isDirectory()) {
|
|
124
133
|
const children = this.walkDir(fullPath, basePath, parseFrontmatter);
|
|
125
134
|
nodes.push({ name: entry.name, path: relPath, type: 'folder', children });
|
|
126
135
|
} else {
|
|
@@ -120,7 +120,7 @@ export class ScopeService {
|
|
|
120
120
|
const id = this.cache.removeByFilePath(filePath);
|
|
121
121
|
if (id !== undefined) {
|
|
122
122
|
if (previous) this.recentlyRemoved.set(id, previous.status);
|
|
123
|
-
this.io.emit('scope:deleted', id);
|
|
123
|
+
this.io.emit('scope:deleted', { id });
|
|
124
124
|
// Clean up stash after a short window (if add never fires, this was a real delete)
|
|
125
125
|
setTimeout(() => this.recentlyRemoved.delete(id), 5000);
|
|
126
126
|
}
|
|
@@ -379,7 +379,7 @@ export class ScopeService {
|
|
|
379
379
|
|
|
380
380
|
/** Promote an icebox idea to planning — assigns a proper sequential scope ID,
|
|
381
381
|
* moves the file, and syncs cache. Returns the new scope ID. */
|
|
382
|
-
promoteIdea(slug: string): { id: number; filePath: string; title: string; description: string } | null {
|
|
382
|
+
promoteIdea(slug: string, targetStatus = 'planning'): { id: number; filePath: string; title: string; description: string } | null {
|
|
383
383
|
const iceboxDir = path.join(this.scopesDir, 'icebox');
|
|
384
384
|
const oldPath = this.findIdeaFile(iceboxDir, slug);
|
|
385
385
|
if (!oldPath) return null;
|
|
@@ -397,15 +397,15 @@ export class ScopeService {
|
|
|
397
397
|
|
|
398
398
|
// Build new path
|
|
399
399
|
const titleSlug = this.slugify(title);
|
|
400
|
-
const
|
|
401
|
-
if (!fs.existsSync(
|
|
400
|
+
const targetDir = path.join(this.scopesDir, targetStatus);
|
|
401
|
+
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
|
|
402
402
|
const newFileName = `${paddedId}-${titleSlug}.md`;
|
|
403
|
-
const newPath = path.join(
|
|
403
|
+
const newPath = path.join(targetDir, newFileName);
|
|
404
404
|
const now = new Date().toISOString().split('T')[0];
|
|
405
405
|
|
|
406
406
|
// Update frontmatter in-place: assign ID and change status (preserve other fields)
|
|
407
407
|
parsed.data.id = newId;
|
|
408
|
-
parsed.data.status =
|
|
408
|
+
parsed.data.status = targetStatus;
|
|
409
409
|
parsed.data.updated = now;
|
|
410
410
|
parsed.data.created = created;
|
|
411
411
|
delete parsed.data.ghost;
|
|
@@ -2,9 +2,11 @@ import type Database from 'better-sqlite3';
|
|
|
2
2
|
import type { Emitter } from '../project-emitter.js';
|
|
3
3
|
import { SprintService } from './sprint-service.js';
|
|
4
4
|
import { ScopeService } from './scope-service.js';
|
|
5
|
-
import { launchInCategorizedTerminal, escapeForAnsiC, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
|
|
5
|
+
import { launchInCategorizedTerminal, escapeForAnsiC, shellQuote, buildSessionName, snapshotSessionPids, discoverNewSession, renameSession } from '../utils/terminal-launcher.js';
|
|
6
6
|
import { resolveDispatchEvent, linkPidToDispatch } from '../utils/dispatch-utils.js';
|
|
7
|
+
import { buildClaudeFlags, buildEnvVarPrefix } from '../utils/flag-builder.js';
|
|
7
8
|
import type { WorkflowEngine } from '../../shared/workflow-engine.js';
|
|
9
|
+
import type { OrbitalConfig } from '../config.js';
|
|
8
10
|
import { createLogger } from '../utils/logger.js';
|
|
9
11
|
|
|
10
12
|
const log = createLogger('sprint');
|
|
@@ -24,6 +26,7 @@ export class SprintOrchestrator {
|
|
|
24
26
|
private scopeService: ScopeService,
|
|
25
27
|
private engine: WorkflowEngine,
|
|
26
28
|
private projectRoot: string,
|
|
29
|
+
private config: OrbitalConfig,
|
|
27
30
|
) {}
|
|
28
31
|
|
|
29
32
|
/** Build execution layers using Kahn's topological sort */
|
|
@@ -112,14 +115,16 @@ export class SprintOrchestrator {
|
|
|
112
115
|
async onScopeReachedDev(scopeId: number): Promise<void> {
|
|
113
116
|
const match = this.sprintService.findActiveSprintForScope(scopeId);
|
|
114
117
|
if (!match) return;
|
|
115
|
-
log.debug('Scope reached dev', { scopeId, sprintId: match.sprint_id });
|
|
116
118
|
|
|
119
|
+
// Batches are managed by BatchOrchestrator — don't dispatch individual scopes
|
|
117
120
|
const sprintId = match.sprint_id;
|
|
121
|
+
const sprint = this.sprintService.getById(sprintId);
|
|
122
|
+
if (!sprint || sprint.group_type === 'batch') return;
|
|
123
|
+
|
|
124
|
+
log.debug('Scope reached dev', { scopeId, sprintId });
|
|
118
125
|
this.sprintService.updateScopeStatus(sprintId, scopeId, 'completed');
|
|
119
126
|
|
|
120
127
|
// Ensure sprint is in 'in_progress' state
|
|
121
|
-
const sprint = this.sprintService.getById(sprintId);
|
|
122
|
-
if (!sprint) return;
|
|
123
128
|
if (sprint.status === 'dispatched') {
|
|
124
129
|
this.sprintService.updateStatus(sprintId, 'in_progress');
|
|
125
130
|
}
|
|
@@ -196,7 +201,7 @@ export class SprintOrchestrator {
|
|
|
196
201
|
const sprint = this.sprintService.getById(sprintId);
|
|
197
202
|
if (!sprint) return null;
|
|
198
203
|
|
|
199
|
-
const layers = sprint.layers ??
|
|
204
|
+
const layers = sprint.layers ?? this.buildExecutionLayers(sprint.scope_ids).layers;
|
|
200
205
|
const sprintSet = new Set(sprint.scope_ids);
|
|
201
206
|
const edges: Array<{ from: number; to: number }> = [];
|
|
202
207
|
|
|
@@ -225,9 +230,15 @@ export class SprintOrchestrator {
|
|
|
225
230
|
const currentScope = this.scopeService.getById(scopeId);
|
|
226
231
|
const previousStatus = currentScope?.status ?? 'implementing';
|
|
227
232
|
|
|
233
|
+
// Resolve command and target status from workflow engine
|
|
234
|
+
const sprint = this.sprintService.getById(sprintId);
|
|
235
|
+
const targetColumn = sprint?.target_column ?? 'backlog';
|
|
236
|
+
const edgeCommand = this.engine.getBatchCommand(targetColumn);
|
|
237
|
+
const targetStatus = this.engine.getBatchTargetStatus(targetColumn);
|
|
238
|
+
|
|
228
239
|
// Record DISPATCH event
|
|
229
240
|
const eventId = crypto.randomUUID();
|
|
230
|
-
const command = `/scope
|
|
241
|
+
const command = edgeCommand ?? `/scope-implement ${scopeId}`;
|
|
231
242
|
this.db.prepare(
|
|
232
243
|
`INSERT INTO events (id, type, scope_id, session_id, agent, data, timestamp)
|
|
233
244
|
VALUES (?, 'DISPATCH', ?, NULL, 'sprint-orchestrator', ?, ?)`,
|
|
@@ -241,7 +252,9 @@ export class SprintOrchestrator {
|
|
|
241
252
|
});
|
|
242
253
|
|
|
243
254
|
// Update scope + sprint_scope status
|
|
244
|
-
|
|
255
|
+
if (targetStatus) {
|
|
256
|
+
this.scopeService.updateStatus(scopeId, targetStatus, 'dispatch');
|
|
257
|
+
}
|
|
245
258
|
this.sprintService.updateScopeStatus(sprintId, scopeId, 'dispatched');
|
|
246
259
|
|
|
247
260
|
// Build scope-aware session name and snapshot PIDs
|
|
@@ -249,9 +262,11 @@ export class SprintOrchestrator {
|
|
|
249
262
|
const sessionName = buildSessionName({ scopeId, title: scopeRow?.title, command });
|
|
250
263
|
const beforePids = snapshotSessionPids(this.projectRoot);
|
|
251
264
|
|
|
252
|
-
// Launch in iTerm — interactive TUI mode
|
|
265
|
+
// Launch in iTerm — interactive TUI mode for full visibility
|
|
253
266
|
const escaped = escapeForAnsiC(command);
|
|
254
|
-
const
|
|
267
|
+
const flagsStr = buildClaudeFlags(this.config.claude.dispatchFlags);
|
|
268
|
+
const envPrefix = buildEnvVarPrefix(this.config.dispatch.envVars);
|
|
269
|
+
const fullCmd = `cd '${shellQuote(this.projectRoot)}' && ${envPrefix}ORBITAL_DISPATCH_ID='${shellQuote(eventId)}' claude ${flagsStr} $'${escaped}'`;
|
|
255
270
|
try {
|
|
256
271
|
await launchInCategorizedTerminal(command, fullCmd, sessionName);
|
|
257
272
|
|
|
@@ -157,10 +157,10 @@ export class SprintService {
|
|
|
157
157
|
return this.buildDetail(row);
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
/** Delete a sprint (
|
|
160
|
+
/** Delete a sprint/batch (assembling, failed, or cancelled) */
|
|
161
161
|
delete(id: number): boolean {
|
|
162
162
|
const row = this.db.prepare('SELECT status FROM sprints WHERE id = ?').get(id) as { status: string } | undefined;
|
|
163
|
-
if (!row ||
|
|
163
|
+
if (!row || !['assembling', 'failed', 'cancelled', 'completed'].includes(row.status)) return false;
|
|
164
164
|
|
|
165
165
|
this.db.prepare('DELETE FROM sprint_scopes WHERE sprint_id = ?').run(id);
|
|
166
166
|
this.db.prepare('DELETE FROM sprints WHERE id = ?').run(id);
|