orbital-command 0.1.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/LICENSE +21 -0
- package/README.md +396 -0
- package/bin/orbital.js +362 -0
- package/dist/assets/WorkflowVisualizer-BZ21PIIF.js +84 -0
- package/dist/assets/WorkflowVisualizer-BZV40eAE.css +1 -0
- package/dist/assets/charts-D__PA1zp.js +72 -0
- package/dist/assets/index-D1G6i0nS.css +1 -0
- package/dist/assets/index-DpItvKpf.js +419 -0
- package/dist/assets/ui-BvF022GT.js +53 -0
- package/dist/assets/vendor-Dzv9lrRc.js +59 -0
- package/dist/index.html +19 -0
- package/dist/scanner-sweep.png +0 -0
- package/dist/server/server/adapters/index.js +34 -0
- package/dist/server/server/adapters/iterm2-adapter.js +29 -0
- package/dist/server/server/adapters/subprocess-adapter.js +21 -0
- package/dist/server/server/adapters/terminal-adapter.js +1 -0
- package/dist/server/server/config.js +156 -0
- package/dist/server/server/database.js +90 -0
- package/dist/server/server/index.js +372 -0
- package/dist/server/server/init.js +811 -0
- package/dist/server/server/parsers/event-parser.js +64 -0
- package/dist/server/server/parsers/scope-parser.js +188 -0
- package/dist/server/server/routes/config-routes.js +163 -0
- package/dist/server/server/routes/data-routes.js +461 -0
- package/dist/server/server/routes/dispatch-routes.js +215 -0
- package/dist/server/server/routes/git-routes.js +92 -0
- package/dist/server/server/routes/scope-routes.js +215 -0
- package/dist/server/server/routes/sprint-routes.js +116 -0
- package/dist/server/server/routes/version-routes.js +130 -0
- package/dist/server/server/routes/workflow-routes.js +185 -0
- package/dist/server/server/schema.js +90 -0
- package/dist/server/server/services/batch-orchestrator.js +253 -0
- package/dist/server/server/services/claude-session-service.js +352 -0
- package/dist/server/server/services/config-service.js +132 -0
- package/dist/server/server/services/deploy-service.js +51 -0
- package/dist/server/server/services/event-service.js +63 -0
- package/dist/server/server/services/gate-service.js +83 -0
- package/dist/server/server/services/git-service.js +309 -0
- package/dist/server/server/services/github-service.js +145 -0
- package/dist/server/server/services/readiness-service.js +184 -0
- package/dist/server/server/services/scope-cache.js +72 -0
- package/dist/server/server/services/scope-service.js +424 -0
- package/dist/server/server/services/sprint-orchestrator.js +312 -0
- package/dist/server/server/services/sprint-service.js +293 -0
- package/dist/server/server/services/workflow-service.js +397 -0
- package/dist/server/server/utils/cc-hooks-parser.js +49 -0
- package/dist/server/server/utils/dispatch-utils.js +305 -0
- package/dist/server/server/utils/logger.js +86 -0
- package/dist/server/server/utils/terminal-launcher.js +388 -0
- package/dist/server/server/utils/worktree-manager.js +98 -0
- package/dist/server/server/watchers/event-watcher.js +81 -0
- package/dist/server/server/watchers/scope-watcher.js +33 -0
- package/dist/server/shared/api-types.js +5 -0
- package/dist/server/shared/default-workflow.json +616 -0
- package/dist/server/shared/workflow-config.js +44 -0
- package/dist/server/shared/workflow-engine.js +353 -0
- package/index.html +15 -0
- package/package.json +110 -0
- package/postcss.config.js +6 -0
- package/schemas/orbital.config.schema.json +83 -0
- package/scripts/postinstall.js +24 -0
- package/scripts/start.sh +20 -0
- package/server/adapters/index.ts +41 -0
- package/server/adapters/iterm2-adapter.ts +37 -0
- package/server/adapters/subprocess-adapter.ts +25 -0
- package/server/adapters/terminal-adapter.ts +24 -0
- package/server/config.ts +234 -0
- package/server/database.ts +107 -0
- package/server/index.ts +452 -0
- package/server/init.ts +891 -0
- package/server/parsers/event-parser.ts +74 -0
- package/server/parsers/scope-parser.ts +240 -0
- package/server/routes/config-routes.ts +182 -0
- package/server/routes/data-routes.ts +548 -0
- package/server/routes/dispatch-routes.ts +275 -0
- package/server/routes/git-routes.ts +112 -0
- package/server/routes/scope-routes.ts +262 -0
- package/server/routes/sprint-routes.ts +142 -0
- package/server/routes/version-routes.ts +156 -0
- package/server/routes/workflow-routes.ts +198 -0
- package/server/schema.ts +90 -0
- package/server/services/batch-orchestrator.ts +286 -0
- package/server/services/claude-session-service.ts +441 -0
- package/server/services/config-service.ts +151 -0
- package/server/services/deploy-service.ts +98 -0
- package/server/services/event-service.ts +98 -0
- package/server/services/gate-service.ts +126 -0
- package/server/services/git-service.ts +391 -0
- package/server/services/github-service.ts +183 -0
- package/server/services/readiness-service.ts +250 -0
- package/server/services/scope-cache.ts +81 -0
- package/server/services/scope-service.ts +476 -0
- package/server/services/sprint-orchestrator.ts +361 -0
- package/server/services/sprint-service.ts +415 -0
- package/server/services/workflow-service.ts +461 -0
- package/server/utils/cc-hooks-parser.ts +70 -0
- package/server/utils/dispatch-utils.ts +395 -0
- package/server/utils/logger.ts +109 -0
- package/server/utils/terminal-launcher.ts +462 -0
- package/server/utils/worktree-manager.ts +104 -0
- package/server/watchers/event-watcher.ts +100 -0
- package/server/watchers/scope-watcher.ts +38 -0
- package/shared/api-types.ts +20 -0
- package/shared/default-workflow.json +616 -0
- package/shared/workflow-config.ts +170 -0
- package/shared/workflow-engine.ts +427 -0
- package/src/App.tsx +33 -0
- package/src/components/AgentBadge.tsx +40 -0
- package/src/components/BatchPreflightModal.tsx +115 -0
- package/src/components/CardDisplayToggle.tsx +74 -0
- package/src/components/ColumnHeaderActions.tsx +55 -0
- package/src/components/ColumnMenu.tsx +99 -0
- package/src/components/DeployHistory.tsx +141 -0
- package/src/components/DispatchModal.tsx +164 -0
- package/src/components/DispatchPopover.tsx +139 -0
- package/src/components/DragOverlay.tsx +25 -0
- package/src/components/DriftSidebar.tsx +140 -0
- package/src/components/EnvironmentStrip.tsx +88 -0
- package/src/components/ErrorBoundary.tsx +62 -0
- package/src/components/FilterChip.tsx +105 -0
- package/src/components/GateIndicator.tsx +33 -0
- package/src/components/IdeaDetailModal.tsx +190 -0
- package/src/components/IdeaFormDialog.tsx +113 -0
- package/src/components/KanbanColumn.tsx +201 -0
- package/src/components/MarkdownRenderer.tsx +114 -0
- package/src/components/NeonGrid.tsx +128 -0
- package/src/components/PromotionQueue.tsx +89 -0
- package/src/components/ScopeCard.tsx +234 -0
- package/src/components/ScopeDetailModal.tsx +255 -0
- package/src/components/ScopeFilterBar.tsx +152 -0
- package/src/components/SearchInput.tsx +102 -0
- package/src/components/SessionPanel.tsx +335 -0
- package/src/components/SprintContainer.tsx +303 -0
- package/src/components/SprintDependencyDialog.tsx +78 -0
- package/src/components/SprintPreflightModal.tsx +138 -0
- package/src/components/StatusBar.tsx +168 -0
- package/src/components/SwimCell.tsx +67 -0
- package/src/components/SwimLaneRow.tsx +94 -0
- package/src/components/SwimlaneBoardView.tsx +108 -0
- package/src/components/VersionBadge.tsx +139 -0
- package/src/components/ViewModeSelector.tsx +114 -0
- package/src/components/config/AgentChip.tsx +53 -0
- package/src/components/config/AgentCreateDialog.tsx +321 -0
- package/src/components/config/AgentEditor.tsx +175 -0
- package/src/components/config/DirectoryTree.tsx +582 -0
- package/src/components/config/FileEditor.tsx +550 -0
- package/src/components/config/HookChip.tsx +50 -0
- package/src/components/config/StageCard.tsx +198 -0
- package/src/components/config/TransitionZone.tsx +173 -0
- package/src/components/config/UnifiedWorkflowPipeline.tsx +216 -0
- package/src/components/config/WorkflowPipeline.tsx +161 -0
- package/src/components/source-control/BranchList.tsx +93 -0
- package/src/components/source-control/BranchPanel.tsx +105 -0
- package/src/components/source-control/CommitLog.tsx +100 -0
- package/src/components/source-control/CommitRow.tsx +47 -0
- package/src/components/source-control/GitHubPanel.tsx +110 -0
- package/src/components/source-control/GitHubSetupGuide.tsx +52 -0
- package/src/components/source-control/GitOverviewBar.tsx +101 -0
- package/src/components/source-control/PullRequestList.tsx +69 -0
- package/src/components/source-control/WorktreeList.tsx +80 -0
- package/src/components/ui/badge.tsx +41 -0
- package/src/components/ui/button.tsx +55 -0
- package/src/components/ui/card.tsx +78 -0
- package/src/components/ui/dialog.tsx +94 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/scroll-area.tsx +54 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/tabs.tsx +52 -0
- package/src/components/ui/toggle-switch.tsx +35 -0
- package/src/components/ui/tooltip.tsx +27 -0
- package/src/components/workflow/AddEdgeDialog.tsx +217 -0
- package/src/components/workflow/AddListDialog.tsx +201 -0
- package/src/components/workflow/ChecklistEditor.tsx +239 -0
- package/src/components/workflow/CommandPrefixManager.tsx +118 -0
- package/src/components/workflow/ConfigSettingsPanel.tsx +189 -0
- package/src/components/workflow/DirectionSelector.tsx +133 -0
- package/src/components/workflow/DispatchConfigPanel.tsx +180 -0
- package/src/components/workflow/EdgeDetailPanel.tsx +236 -0
- package/src/components/workflow/EdgePropertyEditor.tsx +251 -0
- package/src/components/workflow/EditToolbar.tsx +138 -0
- package/src/components/workflow/HookDetailPanel.tsx +250 -0
- package/src/components/workflow/HookExecutionLog.tsx +24 -0
- package/src/components/workflow/HookSourceModal.tsx +129 -0
- package/src/components/workflow/HooksDashboard.tsx +363 -0
- package/src/components/workflow/ListPropertyEditor.tsx +251 -0
- package/src/components/workflow/MigrationPreviewDialog.tsx +237 -0
- package/src/components/workflow/MovementRulesPanel.tsx +188 -0
- package/src/components/workflow/NodeDetailPanel.tsx +245 -0
- package/src/components/workflow/PresetSelector.tsx +414 -0
- package/src/components/workflow/SkillCommandBuilder.tsx +174 -0
- package/src/components/workflow/WorkflowEdgeComponent.tsx +145 -0
- package/src/components/workflow/WorkflowNode.tsx +147 -0
- package/src/components/workflow/graphLayout.ts +186 -0
- package/src/components/workflow/mergeHooks.ts +85 -0
- package/src/components/workflow/useEditHistory.ts +88 -0
- package/src/components/workflow/useWorkflowEditor.ts +262 -0
- package/src/components/workflow/validateConfig.ts +70 -0
- package/src/hooks/useActiveDispatches.ts +198 -0
- package/src/hooks/useBoardSettings.ts +170 -0
- package/src/hooks/useCardDisplay.ts +57 -0
- package/src/hooks/useCcHooks.ts +24 -0
- package/src/hooks/useConfigTree.ts +51 -0
- package/src/hooks/useEnforcementRules.ts +46 -0
- package/src/hooks/useEvents.ts +59 -0
- package/src/hooks/useFileEditor.ts +165 -0
- package/src/hooks/useGates.ts +57 -0
- package/src/hooks/useIdeaActions.ts +53 -0
- package/src/hooks/useKanbanDnd.ts +410 -0
- package/src/hooks/useOrbitalConfig.ts +54 -0
- package/src/hooks/usePipeline.ts +47 -0
- package/src/hooks/usePipelineData.ts +338 -0
- package/src/hooks/useReconnect.ts +25 -0
- package/src/hooks/useScopeFilters.ts +125 -0
- package/src/hooks/useScopeSessions.ts +44 -0
- package/src/hooks/useScopes.ts +67 -0
- package/src/hooks/useSearch.ts +67 -0
- package/src/hooks/useSettings.tsx +187 -0
- package/src/hooks/useSocket.ts +25 -0
- package/src/hooks/useSourceControl.ts +105 -0
- package/src/hooks/useSprintPreflight.ts +55 -0
- package/src/hooks/useSprints.ts +154 -0
- package/src/hooks/useStatusBarHighlight.ts +18 -0
- package/src/hooks/useSwimlaneBoardSettings.ts +104 -0
- package/src/hooks/useTheme.ts +9 -0
- package/src/hooks/useTransitionReadiness.ts +53 -0
- package/src/hooks/useVersion.ts +155 -0
- package/src/hooks/useViolations.ts +65 -0
- package/src/hooks/useWorkflow.tsx +125 -0
- package/src/hooks/useZoomModifier.ts +19 -0
- package/src/index.css +797 -0
- package/src/layouts/DashboardLayout.tsx +113 -0
- package/src/lib/collisionDetection.ts +20 -0
- package/src/lib/scope-fields.ts +61 -0
- package/src/lib/swimlane.ts +146 -0
- package/src/lib/utils.ts +15 -0
- package/src/main.tsx +19 -0
- package/src/socket.ts +11 -0
- package/src/types/index.ts +497 -0
- package/src/views/AgentFeed.tsx +339 -0
- package/src/views/DeployPipeline.tsx +59 -0
- package/src/views/EnforcementView.tsx +378 -0
- package/src/views/PrimitivesConfig.tsx +500 -0
- package/src/views/QualityGates.tsx +1012 -0
- package/src/views/ScopeBoard.tsx +454 -0
- package/src/views/SessionTimeline.tsx +516 -0
- package/src/views/Settings.tsx +183 -0
- package/src/views/SourceControl.tsx +95 -0
- package/src/views/WorkflowVisualizer.tsx +382 -0
- package/tailwind.config.js +161 -0
- package/templates/agents/AUTO-INVOKE.md +180 -0
- package/templates/agents/CONFLICT-RESOLUTION.md +128 -0
- package/templates/agents/QUICK-REFERENCE.md +122 -0
- package/templates/agents/README.md +188 -0
- package/templates/agents/SKILL-TRIGGERS.md +100 -0
- package/templates/agents/blue-team/frontend-designer.md +424 -0
- package/templates/agents/green-team/architect.md +526 -0
- package/templates/agents/green-team/rules-enforcer.md +131 -0
- package/templates/agents/red-team/attacker-learned.md +24 -0
- package/templates/agents/red-team/attacker.md +486 -0
- package/templates/agents/red-team/chaos.md +548 -0
- package/templates/agents/reference/component-registry.md +82 -0
- package/templates/agents/workflows/full-mode.md +218 -0
- package/templates/agents/workflows/quick-mode.md +118 -0
- package/templates/agents/workflows/security-mode.md +283 -0
- package/templates/anti-patterns/dangerous-shortcuts.md +427 -0
- package/templates/config/agent-triggers.json +92 -0
- package/templates/hooks/agent-team-gate.sh +31 -0
- package/templates/hooks/agent-trigger.sh +97 -0
- package/templates/hooks/block-push.sh +66 -0
- package/templates/hooks/block-workarounds.sh +61 -0
- package/templates/hooks/blocker-check.sh +28 -0
- package/templates/hooks/completion-checklist.sh +28 -0
- package/templates/hooks/decision-capture.sh +15 -0
- package/templates/hooks/dependency-check.sh +27 -0
- package/templates/hooks/end-session.sh +31 -0
- package/templates/hooks/exploration-logger.sh +37 -0
- package/templates/hooks/files-changed-summary.sh +37 -0
- package/templates/hooks/get-session-id.sh +49 -0
- package/templates/hooks/git-commit-guard.sh +34 -0
- package/templates/hooks/init-session.sh +93 -0
- package/templates/hooks/orbital-emit.sh +79 -0
- package/templates/hooks/orbital-report-deploy.sh +78 -0
- package/templates/hooks/orbital-report-gates.sh +40 -0
- package/templates/hooks/orbital-report-violation.sh +36 -0
- package/templates/hooks/orbital-scope-update.sh +53 -0
- package/templates/hooks/phase-verify-reminder.sh +26 -0
- package/templates/hooks/review-gate-check.sh +82 -0
- package/templates/hooks/scope-commit-logger.sh +37 -0
- package/templates/hooks/scope-create-cleanup.sh +36 -0
- package/templates/hooks/scope-create-gate.sh +80 -0
- package/templates/hooks/scope-create-tracker.sh +17 -0
- package/templates/hooks/scope-file-sync.sh +53 -0
- package/templates/hooks/scope-gate.sh +35 -0
- package/templates/hooks/scope-helpers.sh +188 -0
- package/templates/hooks/scope-lifecycle-gate.sh +139 -0
- package/templates/hooks/scope-prepare.sh +244 -0
- package/templates/hooks/scope-transition.sh +172 -0
- package/templates/hooks/session-enforcer.sh +143 -0
- package/templates/hooks/time-tracker.sh +33 -0
- package/templates/lessons-learned.md +15 -0
- package/templates/orbital.config.json +35 -0
- package/templates/presets/development.json +42 -0
- package/templates/presets/gitflow.json +712 -0
- package/templates/presets/minimal.json +23 -0
- package/templates/quick/rules.md +218 -0
- package/templates/scopes/_template.md +255 -0
- package/templates/settings-hooks.json +98 -0
- package/templates/skills/git-commit/SKILL.md +85 -0
- package/templates/skills/git-dev/SKILL.md +99 -0
- package/templates/skills/git-hotfix/SKILL.md +223 -0
- package/templates/skills/git-main/SKILL.md +84 -0
- package/templates/skills/git-production/SKILL.md +165 -0
- package/templates/skills/git-staging/SKILL.md +112 -0
- package/templates/skills/scope-create/SKILL.md +81 -0
- package/templates/skills/scope-fix-review/SKILL.md +168 -0
- package/templates/skills/scope-implement/SKILL.md +110 -0
- package/templates/skills/scope-post-review/SKILL.md +144 -0
- package/templates/skills/scope-pre-review/SKILL.md +211 -0
- package/templates/skills/scope-verify/SKILL.md +201 -0
- package/templates/skills/session-init/SKILL.md +62 -0
- package/templates/skills/session-resume/SKILL.md +201 -0
- package/templates/skills/test-checks/SKILL.md +171 -0
- package/templates/skills/test-code-review/SKILL.md +252 -0
- package/tsconfig.json +25 -0
- package/vite.config.ts +38 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { createLogger } from '../utils/logger.js';
|
|
3
|
+
const log = createLogger('event');
|
|
4
|
+
/**
|
|
5
|
+
* Parse a JSON event file from .claude/orbital-events/
|
|
6
|
+
*
|
|
7
|
+
* Handles two formats:
|
|
8
|
+
* - Full format: top-level scope_id, agent, session_id fields
|
|
9
|
+
* - Minimal format: all info nested inside `data` (from orbital-emit.sh)
|
|
10
|
+
*
|
|
11
|
+
* When top-level fields are missing, extracts them from `data`:
|
|
12
|
+
* - data.agent or data.agents[0] → agent
|
|
13
|
+
* - data.scope_id → scope_id
|
|
14
|
+
* - data.session_id → session_id
|
|
15
|
+
*/
|
|
16
|
+
export function parseEventFile(filePath) {
|
|
17
|
+
try {
|
|
18
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
19
|
+
const parsed = JSON.parse(content);
|
|
20
|
+
if (!parsed.id || !parsed.type || !parsed.timestamp) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const data = (parsed.data ?? {});
|
|
24
|
+
return {
|
|
25
|
+
id: String(parsed.id),
|
|
26
|
+
type: String(parsed.type),
|
|
27
|
+
scope_id: extractScopeId(parsed.scope_id, data),
|
|
28
|
+
session_id: extractString(parsed.session_id, data.session_id),
|
|
29
|
+
agent: extractAgent(parsed.agent, data),
|
|
30
|
+
data,
|
|
31
|
+
timestamp: String(parsed.timestamp),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
log.warn('Failed to parse event file', { file: filePath, error: err.message });
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Extract scope_id from top-level or data payload */
|
|
40
|
+
function extractScopeId(topLevel, data) {
|
|
41
|
+
if (topLevel != null && topLevel !== '')
|
|
42
|
+
return Number(topLevel);
|
|
43
|
+
if (data.scope_id != null && data.scope_id !== '')
|
|
44
|
+
return Number(data.scope_id);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
/** Extract agent name from top-level or data.agent / data.agents[0] */
|
|
48
|
+
function extractAgent(topLevel, data) {
|
|
49
|
+
if (typeof topLevel === 'string' && topLevel !== '')
|
|
50
|
+
return topLevel;
|
|
51
|
+
if (typeof data.agent === 'string' && data.agent !== '')
|
|
52
|
+
return data.agent;
|
|
53
|
+
if (Array.isArray(data.agents) && data.agents.length > 0)
|
|
54
|
+
return String(data.agents[0]);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/** Extract a string value from top-level or data fallback */
|
|
58
|
+
function extractString(topLevel, fallback) {
|
|
59
|
+
if (typeof topLevel === 'string' && topLevel !== '')
|
|
60
|
+
return topLevel;
|
|
61
|
+
if (typeof fallback === 'string' && fallback !== '')
|
|
62
|
+
return fallback;
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
import { createLogger } from '../utils/logger.js';
|
|
5
|
+
const log = createLogger('scope');
|
|
6
|
+
const VALID_PRIORITIES = new Set(['critical', 'high', 'medium', 'low']);
|
|
7
|
+
const VALID_SESSION_KEYS = new Set([
|
|
8
|
+
'createScope', 'reviewScope', 'implementScope',
|
|
9
|
+
'verifyScope', 'reviewGate', 'fixReview', 'commit',
|
|
10
|
+
'pushToMain', 'pushToDev', 'pushToStaging', 'pushToProduction',
|
|
11
|
+
]);
|
|
12
|
+
/** Parse and validate the sessions frontmatter field */
|
|
13
|
+
function parseSessions(raw) {
|
|
14
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
15
|
+
return {};
|
|
16
|
+
const result = {};
|
|
17
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
18
|
+
if (VALID_SESSION_KEYS.has(key) && Array.isArray(value)) {
|
|
19
|
+
result[key] = value.filter((v) => typeof v === 'string');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
// Map frontmatter statuses to 9-column board states
|
|
25
|
+
export const STATUS_MAP = {
|
|
26
|
+
'icebox': 'icebox',
|
|
27
|
+
'exploring': 'planning',
|
|
28
|
+
'planning': 'planning',
|
|
29
|
+
'ready': 'backlog',
|
|
30
|
+
'backlog': 'backlog',
|
|
31
|
+
'blocked': 'backlog',
|
|
32
|
+
'in_progress': 'implementing',
|
|
33
|
+
'in-progress': 'implementing',
|
|
34
|
+
'implementing': 'implementing',
|
|
35
|
+
'testing': 'review',
|
|
36
|
+
'review': 'review',
|
|
37
|
+
'complete': 'completed',
|
|
38
|
+
'completed': 'completed',
|
|
39
|
+
'done': 'production',
|
|
40
|
+
'dev': 'dev',
|
|
41
|
+
'staging': 'staging',
|
|
42
|
+
'production': 'production',
|
|
43
|
+
};
|
|
44
|
+
/** Normalize a raw frontmatter status to a valid board status */
|
|
45
|
+
export function normalizeStatus(raw) {
|
|
46
|
+
return STATUS_MAP[raw] ?? raw;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Parse a scope markdown file into structured data.
|
|
50
|
+
* Handles both YAML frontmatter and plain markdown formats.
|
|
51
|
+
*/
|
|
52
|
+
export function parseScopeFile(filePath) {
|
|
53
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
54
|
+
const fileName = path.basename(filePath, '.md');
|
|
55
|
+
const dirName = path.basename(path.dirname(filePath));
|
|
56
|
+
// Extract ID from filename pattern: NNN[suffix]-description.md
|
|
57
|
+
// Suffixes (a-d, X) encode as thousands offset for unique DB keys
|
|
58
|
+
const idMatch = fileName.match(/^(\d+)([a-dA-DxX])?/);
|
|
59
|
+
const fileId = idMatch ? scopeFileId(parseInt(idMatch[1], 10), idMatch[2]) : 0;
|
|
60
|
+
// Skip non-scope files
|
|
61
|
+
if (fileId === 0 && !fileName.startsWith('0')) {
|
|
62
|
+
// Files like _template.md, technical-debt.md, backlog_plan.md
|
|
63
|
+
if (fileName.startsWith('_') || !idMatch)
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
// Try YAML frontmatter first
|
|
67
|
+
const { data: frontmatter, content: markdownBody } = matter(content);
|
|
68
|
+
if (frontmatter && Object.keys(frontmatter).length > 0) {
|
|
69
|
+
return parseFrontmatterScope(frontmatter, markdownBody, filePath, fileId, dirName);
|
|
70
|
+
}
|
|
71
|
+
// Fallback: extract from markdown structure
|
|
72
|
+
return parseMarkdownScope(content, filePath, fileId, dirName);
|
|
73
|
+
}
|
|
74
|
+
function parseFrontmatterScope(fm, body, filePath, fallbackId, dirName) {
|
|
75
|
+
// Prefer filename-derived ID (includes suffix encoding) over frontmatter
|
|
76
|
+
const id = fallbackId || (fm.id ?? 0);
|
|
77
|
+
const rawStatus = String(fm.status ?? inferStatusFromDir(dirName));
|
|
78
|
+
const status = STATUS_MAP[rawStatus] ?? rawStatus;
|
|
79
|
+
return {
|
|
80
|
+
id,
|
|
81
|
+
title: String(fm.title ?? `Scope ${id}`),
|
|
82
|
+
status,
|
|
83
|
+
priority: fm.priority && VALID_PRIORITIES.has(String(fm.priority)) ? String(fm.priority) : null,
|
|
84
|
+
effort_estimate: fm.effort_estimate ? String(fm.effort_estimate) : null,
|
|
85
|
+
category: fm.category ? String(fm.category) : null,
|
|
86
|
+
tags: Array.isArray(fm.tags) ? fm.tags.map(String) : [],
|
|
87
|
+
blocked_by: Array.isArray(fm.blocked_by) ? fm.blocked_by.map(Number) : [],
|
|
88
|
+
blocks: Array.isArray(fm.blocks) ? fm.blocks.map(Number) : [],
|
|
89
|
+
file_path: filePath,
|
|
90
|
+
created_at: fm.created ? String(fm.created) : null,
|
|
91
|
+
updated_at: fm.updated ? String(fm.updated) : null,
|
|
92
|
+
raw_content: body.trim(),
|
|
93
|
+
sessions: parseSessions(fm.sessions),
|
|
94
|
+
is_ghost: fm.ghost === true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function parseMarkdownScope(content, filePath, id, dirName) {
|
|
98
|
+
// Extract title from first # heading
|
|
99
|
+
const titleMatch = content.match(/^#\s+(?:Scope\s+\d+:\s*)?(.+)/m);
|
|
100
|
+
const title = titleMatch ? titleMatch[1].trim() : `Scope ${id}`;
|
|
101
|
+
// Extract priority from markdown
|
|
102
|
+
const priorityMatch = content.match(/##\s*Priority:\s*(?:[🔴🟡🟢⚪]\s*)?(\w+)/i);
|
|
103
|
+
const rawPriority = priorityMatch ? priorityMatch[1].toLowerCase() : null;
|
|
104
|
+
const priority = rawPriority && VALID_PRIORITIES.has(rawPriority) ? rawPriority : null;
|
|
105
|
+
// Extract effort estimate
|
|
106
|
+
const effortMatch = content.match(/##\s*(?:Estimated\s+)?Effort:\s*(.+)/i);
|
|
107
|
+
const effort_estimate = effortMatch ? effortMatch[1].trim() : null;
|
|
108
|
+
// Extract category
|
|
109
|
+
const categoryMatch = content.match(/##\s*Category:\s*(.+)/i);
|
|
110
|
+
const category = categoryMatch ? categoryMatch[1].trim() : null;
|
|
111
|
+
// Determine status from directory or content
|
|
112
|
+
const status = inferStatusFromDir(dirName);
|
|
113
|
+
return {
|
|
114
|
+
id,
|
|
115
|
+
title,
|
|
116
|
+
status,
|
|
117
|
+
priority,
|
|
118
|
+
effort_estimate,
|
|
119
|
+
category,
|
|
120
|
+
tags: [],
|
|
121
|
+
blocked_by: [],
|
|
122
|
+
blocks: [],
|
|
123
|
+
file_path: filePath,
|
|
124
|
+
created_at: null,
|
|
125
|
+
updated_at: null,
|
|
126
|
+
raw_content: content,
|
|
127
|
+
sessions: {},
|
|
128
|
+
is_ghost: false,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** Map filename suffix (a-d, X) to a thousands-digit offset for unique IDs */
|
|
132
|
+
function scopeFileId(base, suffix) {
|
|
133
|
+
if (!suffix)
|
|
134
|
+
return base;
|
|
135
|
+
const lower = suffix.toLowerCase();
|
|
136
|
+
if (lower === 'x')
|
|
137
|
+
return 9000 + base;
|
|
138
|
+
// a=1000, b=2000, c=3000, d=4000
|
|
139
|
+
const offset = (lower.charCodeAt(0) - 96) * 1000;
|
|
140
|
+
return offset + base;
|
|
141
|
+
}
|
|
142
|
+
/** Valid directory statuses — updated at startup from the workflow engine */
|
|
143
|
+
let validDirStatuses = null;
|
|
144
|
+
/** Initialize the valid status set from the workflow engine's list IDs */
|
|
145
|
+
export function setValidStatuses(statuses) {
|
|
146
|
+
validDirStatuses = new Set(statuses);
|
|
147
|
+
}
|
|
148
|
+
function inferStatusFromDir(dirName) {
|
|
149
|
+
if (validDirStatuses) {
|
|
150
|
+
return validDirStatuses.has(dirName) ? dirName : 'planning';
|
|
151
|
+
}
|
|
152
|
+
// Fallback for when engine hasn't initialized yet (shouldn't happen in practice)
|
|
153
|
+
return dirName;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Scan all scope directories and parse all scope files.
|
|
157
|
+
*/
|
|
158
|
+
export function parseAllScopes(scopesDir) {
|
|
159
|
+
const scopes = [];
|
|
160
|
+
if (!fs.existsSync(scopesDir))
|
|
161
|
+
return scopes;
|
|
162
|
+
// Recursively find all .md files
|
|
163
|
+
function scanDir(dir) {
|
|
164
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
const fullPath = path.join(dir, entry.name);
|
|
167
|
+
if (entry.isDirectory()) {
|
|
168
|
+
scanDir(fullPath);
|
|
169
|
+
}
|
|
170
|
+
else if (entry.name.endsWith('.md') && !entry.name.startsWith('_')) {
|
|
171
|
+
const parsed = parseScopeFile(fullPath);
|
|
172
|
+
if (parsed)
|
|
173
|
+
scopes.push(parsed);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
scanDir(scopesDir);
|
|
178
|
+
// Detect ID collisions — last-write-wins but warn on stderr
|
|
179
|
+
const seen = new Map();
|
|
180
|
+
for (const scope of scopes) {
|
|
181
|
+
const existing = seen.get(scope.id);
|
|
182
|
+
if (existing) {
|
|
183
|
+
log.error('Scope ID collision — renumber one of them', { id: scope.id, existing, duplicate: scope.file_path });
|
|
184
|
+
}
|
|
185
|
+
seen.set(scope.id, scope.file_path);
|
|
186
|
+
}
|
|
187
|
+
return scopes.sort((a, b) => a.id - b.id);
|
|
188
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { ConfigService, isValidPrimitiveType } from '../services/config-service.js';
|
|
3
|
+
export function createConfigRoutes({ projectRoot, workflowService: _workflowService, io }) {
|
|
4
|
+
const router = Router();
|
|
5
|
+
const configService = new ConfigService(projectRoot);
|
|
6
|
+
/** Validate :type param and return the primitive type, or send 400 */
|
|
7
|
+
function parseType(typeParam, res) {
|
|
8
|
+
if (!isValidPrimitiveType(typeParam)) {
|
|
9
|
+
res.status(400).json({ success: false, error: `Invalid type "${typeParam}". Must be one of: agents, skills, hooks` });
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return typeParam;
|
|
13
|
+
}
|
|
14
|
+
// GET /config/:type/tree — directory tree with frontmatter
|
|
15
|
+
router.get('/config/:type/tree', (req, res) => {
|
|
16
|
+
const type = parseType(req.params.type, res);
|
|
17
|
+
if (!type)
|
|
18
|
+
return;
|
|
19
|
+
try {
|
|
20
|
+
const basePath = configService.getBasePath(type);
|
|
21
|
+
const tree = configService.scanDirectory(basePath);
|
|
22
|
+
res.json({ success: true, data: tree });
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
res.status(500).json({ success: false, error: errMsg(err) });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
// GET /config/:type/file?path=<relative> — file content
|
|
29
|
+
router.get('/config/:type/file', (req, res) => {
|
|
30
|
+
const type = parseType(req.params.type, res);
|
|
31
|
+
if (!type)
|
|
32
|
+
return;
|
|
33
|
+
const filePath = req.query.path;
|
|
34
|
+
if (!filePath) {
|
|
35
|
+
res.status(400).json({ success: false, error: 'path query parameter is required' });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const basePath = configService.getBasePath(type);
|
|
40
|
+
const content = configService.readFile(basePath, filePath);
|
|
41
|
+
res.json({ success: true, data: { path: filePath, content } });
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
const msg = errMsg(err);
|
|
45
|
+
const status = msg.includes('traversal') ? 403 : msg.includes('ENOENT') || msg.includes('not found') ? 404 : 500;
|
|
46
|
+
res.status(status).json({ success: false, error: msg });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
// PUT /config/:type/file — save file { path, content }
|
|
50
|
+
router.put('/config/:type/file', (req, res) => {
|
|
51
|
+
const type = parseType(req.params.type, res);
|
|
52
|
+
if (!type)
|
|
53
|
+
return;
|
|
54
|
+
const { path: filePath, content } = req.body;
|
|
55
|
+
if (!filePath || content === undefined) {
|
|
56
|
+
res.status(400).json({ success: false, error: 'path and content are required' });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const basePath = configService.getBasePath(type);
|
|
61
|
+
configService.writeFile(basePath, filePath, content);
|
|
62
|
+
io.emit(`config:${type}:changed`, { action: 'updated', path: filePath });
|
|
63
|
+
res.json({ success: true });
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
const msg = errMsg(err);
|
|
67
|
+
const status = msg.includes('traversal') ? 403 : msg.includes('not found') ? 404 : 500;
|
|
68
|
+
res.status(status).json({ success: false, error: msg });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
// POST /config/:type/file — create file { path, content }
|
|
72
|
+
router.post('/config/:type/file', (req, res) => {
|
|
73
|
+
const type = parseType(req.params.type, res);
|
|
74
|
+
if (!type)
|
|
75
|
+
return;
|
|
76
|
+
const { path: filePath, content } = req.body;
|
|
77
|
+
if (!filePath || content === undefined) {
|
|
78
|
+
res.status(400).json({ success: false, error: 'path and content are required' });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const basePath = configService.getBasePath(type);
|
|
83
|
+
configService.createFile(basePath, filePath, content);
|
|
84
|
+
io.emit(`config:${type}:changed`, { action: 'created', path: filePath });
|
|
85
|
+
res.status(201).json({ success: true });
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
const msg = errMsg(err);
|
|
89
|
+
const status = msg.includes('traversal') ? 403 : msg.includes('already exists') ? 409 : 500;
|
|
90
|
+
res.status(status).json({ success: false, error: msg });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
// DELETE /config/:type/file?path=<relative> — delete file
|
|
94
|
+
router.delete('/config/:type/file', (req, res) => {
|
|
95
|
+
const type = parseType(req.params.type, res);
|
|
96
|
+
if (!type)
|
|
97
|
+
return;
|
|
98
|
+
const filePath = req.query.path;
|
|
99
|
+
if (!filePath) {
|
|
100
|
+
res.status(400).json({ success: false, error: 'path query parameter is required' });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const basePath = configService.getBasePath(type);
|
|
105
|
+
configService.deleteFile(basePath, filePath);
|
|
106
|
+
io.emit(`config:${type}:changed`, { action: 'deleted', path: filePath });
|
|
107
|
+
res.json({ success: true });
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
const msg = errMsg(err);
|
|
111
|
+
const status = msg.includes('traversal') ? 403 : msg.includes('not found') ? 404 : msg.includes('directory') ? 400 : 500;
|
|
112
|
+
res.status(status).json({ success: false, error: msg });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
// POST /config/:type/rename — rename { oldPath, newPath }
|
|
116
|
+
router.post('/config/:type/rename', (req, res) => {
|
|
117
|
+
const type = parseType(req.params.type, res);
|
|
118
|
+
if (!type)
|
|
119
|
+
return;
|
|
120
|
+
const { oldPath, newPath } = req.body;
|
|
121
|
+
if (!oldPath || !newPath) {
|
|
122
|
+
res.status(400).json({ success: false, error: 'oldPath and newPath are required' });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const basePath = configService.getBasePath(type);
|
|
127
|
+
configService.renameFile(basePath, oldPath, newPath);
|
|
128
|
+
io.emit(`config:${type}:changed`, { action: 'renamed', oldPath, newPath });
|
|
129
|
+
res.json({ success: true });
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
const msg = errMsg(err);
|
|
133
|
+
const status = msg.includes('traversal') ? 403 : msg.includes('not found') ? 404 : msg.includes('already exists') ? 409 : 500;
|
|
134
|
+
res.status(status).json({ success: false, error: msg });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
// POST /config/:type/folder — create folder { path }
|
|
138
|
+
router.post('/config/:type/folder', (req, res) => {
|
|
139
|
+
const type = parseType(req.params.type, res);
|
|
140
|
+
if (!type)
|
|
141
|
+
return;
|
|
142
|
+
const { path: folderPath } = req.body;
|
|
143
|
+
if (!folderPath) {
|
|
144
|
+
res.status(400).json({ success: false, error: 'path is required' });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const basePath = configService.getBasePath(type);
|
|
149
|
+
configService.createFolder(basePath, folderPath);
|
|
150
|
+
io.emit(`config:${type}:changed`, { action: 'folder-created', path: folderPath });
|
|
151
|
+
res.status(201).json({ success: true });
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
const msg = errMsg(err);
|
|
155
|
+
const status = msg.includes('traversal') ? 403 : msg.includes('already exists') ? 409 : 500;
|
|
156
|
+
res.status(status).json({ success: false, error: msg });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
return router;
|
|
160
|
+
}
|
|
161
|
+
function errMsg(err) {
|
|
162
|
+
return err instanceof Error ? err.message : String(err);
|
|
163
|
+
}
|