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,309 @@
|
|
|
1
|
+
import { execFile as execFileCb } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { listWorktrees } from '../utils/worktree-manager.js';
|
|
4
|
+
const execFile = promisify(execFileCb);
|
|
5
|
+
const CACHE_TTL = 60_000; // 60 seconds
|
|
6
|
+
function cached(cache, key) {
|
|
7
|
+
const entry = cache.get(key);
|
|
8
|
+
if (entry && Date.now() - entry.ts < CACHE_TTL)
|
|
9
|
+
return entry.data;
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
function setCache(cache, key, data) {
|
|
13
|
+
cache.set(key, { data, ts: Date.now() });
|
|
14
|
+
}
|
|
15
|
+
// ─── Service ────────────────────────────────────────────────
|
|
16
|
+
const SCOPE_BRANCH_RE = /(?:feat|fix|scope)[/-](?:scope-)?(\d+)/;
|
|
17
|
+
export class GitService {
|
|
18
|
+
projectRoot;
|
|
19
|
+
scopeCache;
|
|
20
|
+
cache = new Map();
|
|
21
|
+
constructor(projectRoot, scopeCache) {
|
|
22
|
+
this.projectRoot = projectRoot;
|
|
23
|
+
this.scopeCache = scopeCache;
|
|
24
|
+
}
|
|
25
|
+
async git(args, cwd) {
|
|
26
|
+
// Uses execFile (not exec) — safe against shell injection
|
|
27
|
+
const { stdout } = await execFile('git', args, {
|
|
28
|
+
cwd: cwd ?? this.projectRoot,
|
|
29
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
30
|
+
});
|
|
31
|
+
return stdout;
|
|
32
|
+
}
|
|
33
|
+
// ─── Overview ──────────────────────────────────────────────
|
|
34
|
+
async getOverview(branchingMode) {
|
|
35
|
+
const cacheKey = `overview:${branchingMode}`;
|
|
36
|
+
const hit = cached(this.cache, cacheKey);
|
|
37
|
+
if (hit)
|
|
38
|
+
return hit;
|
|
39
|
+
const [branchRaw, statusRaw] = await Promise.all([
|
|
40
|
+
this.git(['branch', '--show-current']).catch(() => ''),
|
|
41
|
+
this.git(['status', '--porcelain']).catch(() => ''),
|
|
42
|
+
]);
|
|
43
|
+
const currentBranch = branchRaw.trim() || '(detached)';
|
|
44
|
+
const dirty = statusRaw.trim().length > 0;
|
|
45
|
+
const detached = !branchRaw.trim();
|
|
46
|
+
// Main HEAD
|
|
47
|
+
let mainHead = null;
|
|
48
|
+
try {
|
|
49
|
+
const raw = await this.git(['log', 'HEAD', '-1', '--format=%H|%aI|%s']);
|
|
50
|
+
const [sha, date, ...msgParts] = raw.trim().split('|');
|
|
51
|
+
if (sha)
|
|
52
|
+
mainHead = { sha, message: msgParts.join('|'), date };
|
|
53
|
+
}
|
|
54
|
+
catch { /* no commits yet */ }
|
|
55
|
+
// Ahead/behind relative to origin/main (or origin/master)
|
|
56
|
+
let aheadBehind = null;
|
|
57
|
+
if (!detached) {
|
|
58
|
+
try {
|
|
59
|
+
const raw = await this.git(['rev-list', '--left-right', '--count', `origin/main...${currentBranch}`]);
|
|
60
|
+
const [behind, ahead] = raw.trim().split('\t').map(Number);
|
|
61
|
+
aheadBehind = { ahead: ahead ?? 0, behind: behind ?? 0 };
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
try {
|
|
65
|
+
const raw = await this.git(['rev-list', '--left-right', '--count', `origin/master...${currentBranch}`]);
|
|
66
|
+
const [behind, ahead] = raw.trim().split('\t').map(Number);
|
|
67
|
+
aheadBehind = { ahead: ahead ?? 0, behind: behind ?? 0 };
|
|
68
|
+
}
|
|
69
|
+
catch { /* no remote tracking */ }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Worktree and feature branch counts
|
|
73
|
+
let worktreeCount = 0;
|
|
74
|
+
let featureBranchCount = 0;
|
|
75
|
+
try {
|
|
76
|
+
const wts = await listWorktrees(this.projectRoot);
|
|
77
|
+
worktreeCount = wts.length;
|
|
78
|
+
}
|
|
79
|
+
catch { /* ok */ }
|
|
80
|
+
try {
|
|
81
|
+
const raw = await this.git(['branch', '--format=%(refname:short)']);
|
|
82
|
+
const branches = raw.trim().split('\n').filter(Boolean);
|
|
83
|
+
featureBranchCount = branches.filter(b => SCOPE_BRANCH_RE.test(b) || b.startsWith('feat/')).length;
|
|
84
|
+
}
|
|
85
|
+
catch { /* ok */ }
|
|
86
|
+
const result = {
|
|
87
|
+
branchingMode,
|
|
88
|
+
currentBranch,
|
|
89
|
+
dirty,
|
|
90
|
+
detached,
|
|
91
|
+
mainHead,
|
|
92
|
+
aheadBehind,
|
|
93
|
+
worktreeCount,
|
|
94
|
+
featureBranchCount,
|
|
95
|
+
};
|
|
96
|
+
setCache(this.cache, cacheKey, result);
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
// ─── Commits ──────────────────────────────────────────────
|
|
100
|
+
async getCommits(opts = {}) {
|
|
101
|
+
const { branch, limit = 50, offset = 0 } = opts;
|
|
102
|
+
const cacheKey = `commits:${branch ?? 'all'}:${limit}:${offset}`;
|
|
103
|
+
const hit = cached(this.cache, cacheKey);
|
|
104
|
+
if (hit)
|
|
105
|
+
return hit;
|
|
106
|
+
const args = ['log', '--format=%H|%h|%aI|%an|%s|%D'];
|
|
107
|
+
if (branch && branch !== 'all') {
|
|
108
|
+
args.push(branch);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
args.push('--all');
|
|
112
|
+
}
|
|
113
|
+
args.push(`--skip=${offset}`, `-${limit}`);
|
|
114
|
+
let raw;
|
|
115
|
+
try {
|
|
116
|
+
raw = await this.git(args);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
const commits = [];
|
|
122
|
+
for (const line of raw.trim().split('\n')) {
|
|
123
|
+
if (!line)
|
|
124
|
+
continue;
|
|
125
|
+
const parts = line.split('|');
|
|
126
|
+
const sha = parts[0];
|
|
127
|
+
const shortSha = parts[1];
|
|
128
|
+
const date = parts[2];
|
|
129
|
+
const author = parts[3];
|
|
130
|
+
const message = parts[4];
|
|
131
|
+
const refStr = parts.slice(5).join('|');
|
|
132
|
+
const refs = refStr
|
|
133
|
+
? refStr.split(',').map(r => r.trim()).filter(Boolean)
|
|
134
|
+
: [];
|
|
135
|
+
// Extract scope ID from refs or message
|
|
136
|
+
let scopeId = null;
|
|
137
|
+
for (const ref of refs) {
|
|
138
|
+
const m = SCOPE_BRANCH_RE.exec(ref);
|
|
139
|
+
if (m) {
|
|
140
|
+
scopeId = parseInt(m[1]);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!scopeId) {
|
|
145
|
+
const m = SCOPE_BRANCH_RE.exec(message);
|
|
146
|
+
if (m)
|
|
147
|
+
scopeId = parseInt(m[1]);
|
|
148
|
+
}
|
|
149
|
+
// Derive branch from first ref that looks like a branch
|
|
150
|
+
let branchName = '';
|
|
151
|
+
for (const ref of refs) {
|
|
152
|
+
const cleaned = ref.replace(/^HEAD -> /, '').replace(/^origin\//, '');
|
|
153
|
+
if (cleaned && !cleaned.startsWith('tag:')) {
|
|
154
|
+
branchName = cleaned;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
commits.push({ sha, shortSha, message, author, date, branch: branchName, scopeId, refs });
|
|
159
|
+
}
|
|
160
|
+
setCache(this.cache, cacheKey, commits);
|
|
161
|
+
return commits;
|
|
162
|
+
}
|
|
163
|
+
// ─── Branches ──────────────────────────────────────────────
|
|
164
|
+
async getBranches() {
|
|
165
|
+
const hit = cached(this.cache, 'branches');
|
|
166
|
+
if (hit)
|
|
167
|
+
return hit;
|
|
168
|
+
let raw;
|
|
169
|
+
try {
|
|
170
|
+
raw = await this.git([
|
|
171
|
+
'branch', '-a',
|
|
172
|
+
'--format=%(HEAD)|%(refname:short)|%(objectname:short)|%(committerdate:iso-strict)|%(subject)',
|
|
173
|
+
]);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
const STALE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
180
|
+
const branches = [];
|
|
181
|
+
for (const line of raw.trim().split('\n')) {
|
|
182
|
+
if (!line)
|
|
183
|
+
continue;
|
|
184
|
+
const [headMarker, name, headSha, headDate, ...msgParts] = line.split('|');
|
|
185
|
+
if (!name || name.includes('HEAD'))
|
|
186
|
+
continue;
|
|
187
|
+
const isCurrent = headMarker === '*';
|
|
188
|
+
const isRemote = name.startsWith('remotes/') || name.startsWith('origin/');
|
|
189
|
+
const cleanName = name.replace(/^remotes\//, '');
|
|
190
|
+
// Skip remote duplicates of local branches
|
|
191
|
+
if (isRemote) {
|
|
192
|
+
const localName = cleanName.replace(/^origin\//, '');
|
|
193
|
+
if (branches.some(b => !b.isRemote && b.name === localName))
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const scopeMatch = SCOPE_BRANCH_RE.exec(cleanName);
|
|
197
|
+
const scopeId = scopeMatch ? parseInt(scopeMatch[1]) : null;
|
|
198
|
+
const isStale = headDate ? (now - new Date(headDate).getTime() > STALE_MS) : false;
|
|
199
|
+
// Ahead/behind relative to origin/main
|
|
200
|
+
let aheadBehind = null;
|
|
201
|
+
if (!isRemote) {
|
|
202
|
+
try {
|
|
203
|
+
const countRaw = await this.git(['rev-list', '--left-right', '--count', `origin/main...${name}`]);
|
|
204
|
+
const [behind, ahead] = countRaw.trim().split('\t').map(Number);
|
|
205
|
+
aheadBehind = { ahead: ahead ?? 0, behind: behind ?? 0 };
|
|
206
|
+
}
|
|
207
|
+
catch { /* no remote */ }
|
|
208
|
+
}
|
|
209
|
+
branches.push({
|
|
210
|
+
name: cleanName,
|
|
211
|
+
isRemote,
|
|
212
|
+
isCurrent,
|
|
213
|
+
headSha: headSha ?? '',
|
|
214
|
+
headMessage: msgParts.join('|'),
|
|
215
|
+
headDate: headDate ?? '',
|
|
216
|
+
aheadBehind,
|
|
217
|
+
scopeId,
|
|
218
|
+
isStale,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
setCache(this.cache, 'branches', branches);
|
|
222
|
+
return branches;
|
|
223
|
+
}
|
|
224
|
+
// ─── Enhanced Worktrees ────────────────────────────────────
|
|
225
|
+
async getEnhancedWorktrees() {
|
|
226
|
+
const hit = cached(this.cache, 'worktrees-enhanced');
|
|
227
|
+
if (hit)
|
|
228
|
+
return hit;
|
|
229
|
+
let wts;
|
|
230
|
+
try {
|
|
231
|
+
wts = await listWorktrees(this.projectRoot);
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
const results = [];
|
|
237
|
+
for (const wt of wts) {
|
|
238
|
+
let head = '';
|
|
239
|
+
try {
|
|
240
|
+
head = (await this.git(['rev-parse', '--short', 'HEAD'], wt.path)).trim();
|
|
241
|
+
}
|
|
242
|
+
catch { /* ok */ }
|
|
243
|
+
let dirty = false;
|
|
244
|
+
try {
|
|
245
|
+
const status = (await this.git(['status', '--porcelain'], wt.path)).trim();
|
|
246
|
+
dirty = status.length > 0;
|
|
247
|
+
}
|
|
248
|
+
catch { /* ok */ }
|
|
249
|
+
let aheadBehind = null;
|
|
250
|
+
try {
|
|
251
|
+
const branchName = wt.branch.replace(/^refs\/heads\//, '');
|
|
252
|
+
const countRaw = await this.git(['rev-list', '--left-right', '--count', `origin/main...${branchName}`], wt.path);
|
|
253
|
+
const [behind, ahead] = countRaw.trim().split('\t').map(Number);
|
|
254
|
+
aheadBehind = { ahead: ahead ?? 0, behind: behind ?? 0 };
|
|
255
|
+
}
|
|
256
|
+
catch { /* ok */ }
|
|
257
|
+
const scope = wt.scopeId ? this.scopeCache.getById(wt.scopeId) : null;
|
|
258
|
+
results.push({
|
|
259
|
+
path: wt.path,
|
|
260
|
+
branch: wt.branch.replace(/^refs\/heads\//, ''),
|
|
261
|
+
head,
|
|
262
|
+
scopeId: wt.scopeId,
|
|
263
|
+
scopeTitle: scope?.title ?? null,
|
|
264
|
+
scopeStatus: scope?.status ?? null,
|
|
265
|
+
dirty,
|
|
266
|
+
aheadBehind,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
setCache(this.cache, 'worktrees-enhanced', results);
|
|
270
|
+
return results;
|
|
271
|
+
}
|
|
272
|
+
// ─── Dynamic Drift ─────────────────────────────────────────
|
|
273
|
+
async getDrift(gitBranches) {
|
|
274
|
+
const cacheKey = `drift:${gitBranches.map(b => `${b.from}-${b.to}`).join(',')}`;
|
|
275
|
+
const hit = cached(this.cache, cacheKey);
|
|
276
|
+
if (hit)
|
|
277
|
+
return hit;
|
|
278
|
+
const pairs = [];
|
|
279
|
+
for (const { from, to } of gitBranches) {
|
|
280
|
+
try {
|
|
281
|
+
const raw = await this.git([
|
|
282
|
+
'log', `origin/${from}`, '--not', `origin/${to}`,
|
|
283
|
+
'--reverse', '--format=%H|%aI|%s|%an',
|
|
284
|
+
]);
|
|
285
|
+
const commits = raw.trim().split('\n').filter(Boolean).map(line => {
|
|
286
|
+
const [sha, date, ...rest] = line.split('|');
|
|
287
|
+
return { sha, date, message: rest.slice(0, -1).join('|'), author: rest[rest.length - 1] };
|
|
288
|
+
});
|
|
289
|
+
pairs.push({ from, to, count: commits.length, commits });
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
pairs.push({ from, to, count: 0, commits: [] });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
setCache(this.cache, cacheKey, pairs);
|
|
296
|
+
return pairs;
|
|
297
|
+
}
|
|
298
|
+
// ─── Git Status Polling ────────────────────────────────────
|
|
299
|
+
async getStatusHash() {
|
|
300
|
+
const [head, dirty] = await Promise.all([
|
|
301
|
+
this.git(['rev-parse', 'HEAD']).catch(() => 'none'),
|
|
302
|
+
this.git(['status', '--porcelain']).catch(() => ''),
|
|
303
|
+
]);
|
|
304
|
+
return `${head.trim()}:${dirty.trim().length > 0 ? 'dirty' : 'clean'}`;
|
|
305
|
+
}
|
|
306
|
+
clearCache() {
|
|
307
|
+
this.cache.clear();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { execFile as execFileCb } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
// Uses execFile (not exec) — safe against shell injection
|
|
4
|
+
const execFile = promisify(execFileCb);
|
|
5
|
+
// ─── Service ────────────────────────────────────────────────
|
|
6
|
+
const CACHE_TTL = 30_000; // 30 seconds
|
|
7
|
+
const SCOPE_ID_RE = /(?:scope|feat)[/-](\d+)/gi;
|
|
8
|
+
export class GitHubService {
|
|
9
|
+
projectRoot;
|
|
10
|
+
statusCache = null;
|
|
11
|
+
prCache = null;
|
|
12
|
+
constructor(projectRoot) {
|
|
13
|
+
this.projectRoot = projectRoot;
|
|
14
|
+
}
|
|
15
|
+
async gh(args) {
|
|
16
|
+
const { stdout } = await execFile('gh', args, {
|
|
17
|
+
cwd: this.projectRoot,
|
|
18
|
+
timeout: 10_000,
|
|
19
|
+
});
|
|
20
|
+
return stdout;
|
|
21
|
+
}
|
|
22
|
+
async ghAvailable() {
|
|
23
|
+
try {
|
|
24
|
+
await execFile('which', ['gh']);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async getStatus() {
|
|
32
|
+
if (this.statusCache && Date.now() - this.statusCache.ts < CACHE_TTL) {
|
|
33
|
+
return this.statusCache.data;
|
|
34
|
+
}
|
|
35
|
+
const available = await this.ghAvailable();
|
|
36
|
+
if (!available) {
|
|
37
|
+
const result = {
|
|
38
|
+
connected: false,
|
|
39
|
+
authUser: null,
|
|
40
|
+
repo: null,
|
|
41
|
+
openPRs: 0,
|
|
42
|
+
error: 'gh CLI not installed',
|
|
43
|
+
};
|
|
44
|
+
this.statusCache = { data: result, ts: Date.now() };
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
// Check auth
|
|
48
|
+
let authUser = null;
|
|
49
|
+
try {
|
|
50
|
+
const whoami = await this.gh(['api', 'user', '--jq', '.login']);
|
|
51
|
+
authUser = whoami.trim() || null;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
const result = {
|
|
55
|
+
connected: false,
|
|
56
|
+
authUser: null,
|
|
57
|
+
repo: null,
|
|
58
|
+
openPRs: 0,
|
|
59
|
+
error: 'gh not authenticated — run `gh auth login`',
|
|
60
|
+
};
|
|
61
|
+
this.statusCache = { data: result, ts: Date.now() };
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
// Get repo info
|
|
65
|
+
let repo = null;
|
|
66
|
+
try {
|
|
67
|
+
const raw = await this.gh([
|
|
68
|
+
'repo', 'view', '--json', 'owner,name,defaultBranchRef,visibility,url',
|
|
69
|
+
]);
|
|
70
|
+
const parsed = JSON.parse(raw);
|
|
71
|
+
repo = {
|
|
72
|
+
owner: parsed.owner?.login ?? '',
|
|
73
|
+
name: parsed.name ?? '',
|
|
74
|
+
fullName: `${parsed.owner?.login ?? ''}/${parsed.name ?? ''}`,
|
|
75
|
+
defaultBranch: parsed.defaultBranchRef?.name ?? 'main',
|
|
76
|
+
visibility: (parsed.visibility ?? 'private').toLowerCase(),
|
|
77
|
+
url: parsed.url ?? '',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
const result = {
|
|
82
|
+
connected: false,
|
|
83
|
+
authUser,
|
|
84
|
+
repo: null,
|
|
85
|
+
openPRs: 0,
|
|
86
|
+
error: 'Not a GitHub repository',
|
|
87
|
+
};
|
|
88
|
+
this.statusCache = { data: result, ts: Date.now() };
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
// Get open PR count
|
|
92
|
+
let openPRs = 0;
|
|
93
|
+
try {
|
|
94
|
+
const raw = await this.gh(['pr', 'list', '--state', 'open', '--json', 'number', '--limit', '100']);
|
|
95
|
+
const parsed = JSON.parse(raw);
|
|
96
|
+
openPRs = Array.isArray(parsed) ? parsed.length : 0;
|
|
97
|
+
}
|
|
98
|
+
catch { /* ok */ }
|
|
99
|
+
const result = { connected: true, authUser, repo, openPRs, error: null };
|
|
100
|
+
this.statusCache = { data: result, ts: Date.now() };
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
async getOpenPRs() {
|
|
104
|
+
if (this.prCache && Date.now() - this.prCache.ts < CACHE_TTL) {
|
|
105
|
+
return this.prCache.data;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const raw = await this.gh([
|
|
109
|
+
'pr', 'list', '--state', 'open', '--json',
|
|
110
|
+
'number,title,author,headRefName,baseRefName,state,url,createdAt',
|
|
111
|
+
'--limit', '30',
|
|
112
|
+
]);
|
|
113
|
+
const parsed = JSON.parse(raw);
|
|
114
|
+
const prs = parsed.map(pr => {
|
|
115
|
+
const title = String(pr.title ?? '');
|
|
116
|
+
const branch = String(pr.headRefName ?? '');
|
|
117
|
+
const scopeIds = [];
|
|
118
|
+
const sources = `${title} ${branch}`;
|
|
119
|
+
let m;
|
|
120
|
+
SCOPE_ID_RE.lastIndex = 0;
|
|
121
|
+
while ((m = SCOPE_ID_RE.exec(sources)) !== null) {
|
|
122
|
+
const id = parseInt(m[1]);
|
|
123
|
+
if (!scopeIds.includes(id))
|
|
124
|
+
scopeIds.push(id);
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
number: Number(pr.number),
|
|
128
|
+
title,
|
|
129
|
+
author: typeof pr.author === 'object' && pr.author ? String(pr.author.login ?? '') : '',
|
|
130
|
+
branch,
|
|
131
|
+
baseBranch: String(pr.baseRefName ?? ''),
|
|
132
|
+
state: String(pr.state ?? ''),
|
|
133
|
+
url: String(pr.url ?? ''),
|
|
134
|
+
createdAt: String(pr.createdAt ?? ''),
|
|
135
|
+
scopeIds,
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
this.prCache = { data: prs, ts: Date.now() };
|
|
139
|
+
return prs;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
// ─── ReadinessService ───────────────────────────────────
|
|
4
|
+
export class ReadinessService {
|
|
5
|
+
scopeService;
|
|
6
|
+
gateService;
|
|
7
|
+
engine;
|
|
8
|
+
projectRoot;
|
|
9
|
+
constructor(scopeService, gateService, engine, projectRoot) {
|
|
10
|
+
this.scopeService = scopeService;
|
|
11
|
+
this.gateService = gateService;
|
|
12
|
+
this.engine = engine;
|
|
13
|
+
this.projectRoot = projectRoot;
|
|
14
|
+
}
|
|
15
|
+
getReadiness(scopeId) {
|
|
16
|
+
const scope = this.scopeService.getById(scopeId);
|
|
17
|
+
if (!scope)
|
|
18
|
+
return null;
|
|
19
|
+
const targets = this.engine.getValidTargets(scope.status);
|
|
20
|
+
const transitions = [];
|
|
21
|
+
for (const to of targets) {
|
|
22
|
+
const edge = this.engine.findEdge(scope.status, to);
|
|
23
|
+
if (!edge)
|
|
24
|
+
continue;
|
|
25
|
+
// Only show forward and shortcut transitions (not backward)
|
|
26
|
+
if (edge.direction === 'backward')
|
|
27
|
+
continue;
|
|
28
|
+
const hooks = this.evaluateHooks(scope, edge);
|
|
29
|
+
const gates = this.getGatesForScope(scopeId);
|
|
30
|
+
const blockers = this.computeBlockers(hooks, scope);
|
|
31
|
+
transitions.push({
|
|
32
|
+
from: scope.status,
|
|
33
|
+
to,
|
|
34
|
+
edge,
|
|
35
|
+
hooks,
|
|
36
|
+
gates,
|
|
37
|
+
ready: blockers.length === 0,
|
|
38
|
+
blockers,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
scope_id: scopeId,
|
|
43
|
+
current_status: scope.status,
|
|
44
|
+
transitions,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
evaluateHooks(scope, edge) {
|
|
48
|
+
const hooks = this.engine.getHooksForEdge(edge.from, edge.to);
|
|
49
|
+
return hooks.map((hook) => {
|
|
50
|
+
const enforcement = this.engine.getHookEnforcement(hook);
|
|
51
|
+
const { status, reason } = this.evaluateHook(hook.id, scope, edge);
|
|
52
|
+
return {
|
|
53
|
+
id: hook.id,
|
|
54
|
+
label: hook.label,
|
|
55
|
+
category: hook.category,
|
|
56
|
+
enforcement,
|
|
57
|
+
status,
|
|
58
|
+
reason,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
evaluateHook(hookId, scope, edge) {
|
|
63
|
+
switch (hookId) {
|
|
64
|
+
case 'session-enforcer':
|
|
65
|
+
return this.checkSessionEnforcer(scope, edge);
|
|
66
|
+
case 'review-gate-check':
|
|
67
|
+
return this.checkReviewGate(scope);
|
|
68
|
+
case 'completion-checklist':
|
|
69
|
+
return this.checkCompletionChecklist(scope);
|
|
70
|
+
case 'blocker-check':
|
|
71
|
+
return this.checkBlockers(scope);
|
|
72
|
+
case 'dependency-check':
|
|
73
|
+
return this.checkDependencies(scope);
|
|
74
|
+
case 'scope-create-gate':
|
|
75
|
+
return this.checkScopeStructure(scope);
|
|
76
|
+
case 'scope-transition':
|
|
77
|
+
return { status: 'pass', reason: 'Lifecycle hook (runs on transition)' };
|
|
78
|
+
case 'orbital-scope-update':
|
|
79
|
+
case 'scope-commit-logger':
|
|
80
|
+
return { status: 'pass', reason: 'Observer (post-transition)' };
|
|
81
|
+
default:
|
|
82
|
+
return { status: 'unknown', reason: 'No pre-check available' };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
checkSessionEnforcer(scope, edge) {
|
|
86
|
+
const targetList = this.engine.getList(edge.to);
|
|
87
|
+
const sessionKey = targetList?.sessionKey;
|
|
88
|
+
if (!sessionKey)
|
|
89
|
+
return { status: 'pass', reason: 'No session key required' };
|
|
90
|
+
const sessions = scope.sessions ?? {};
|
|
91
|
+
const recorded = sessions[sessionKey];
|
|
92
|
+
if (Array.isArray(recorded) && recorded.length > 0) {
|
|
93
|
+
return { status: 'pass', reason: `Session recorded (${recorded.length} session(s))` };
|
|
94
|
+
}
|
|
95
|
+
return { status: 'fail', reason: `No '${sessionKey}' session recorded in scope frontmatter` };
|
|
96
|
+
}
|
|
97
|
+
checkReviewGate(scope) {
|
|
98
|
+
const paddedId = String(scope.id).padStart(3, '0');
|
|
99
|
+
const verdictPath = path.join(this.projectRoot, '.claude', 'review-verdicts', `${paddedId}.json`);
|
|
100
|
+
if (!fs.existsSync(verdictPath)) {
|
|
101
|
+
return { status: 'fail', reason: 'No review verdict file found' };
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const verdict = JSON.parse(fs.readFileSync(verdictPath, 'utf-8'));
|
|
105
|
+
if (verdict.verdict === 'PASS') {
|
|
106
|
+
return { status: 'pass', reason: 'Review verdict: PASS' };
|
|
107
|
+
}
|
|
108
|
+
return { status: 'fail', reason: `Review verdict: ${verdict.verdict ?? 'unknown'}` };
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return { status: 'fail', reason: 'Failed to parse review verdict file' };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
checkCompletionChecklist(scope) {
|
|
115
|
+
const content = scope.raw_content ?? '';
|
|
116
|
+
const unchecked = (content.match(/^- \[ \]/gm) ?? []).length;
|
|
117
|
+
const checked = (content.match(/^- \[x\]/gim) ?? []).length;
|
|
118
|
+
if (checked + unchecked === 0) {
|
|
119
|
+
return { status: 'unknown', reason: 'No checklist items found in scope' };
|
|
120
|
+
}
|
|
121
|
+
if (unchecked > 0) {
|
|
122
|
+
return { status: 'fail', reason: `${unchecked} unchecked item(s) in DoD checklist` };
|
|
123
|
+
}
|
|
124
|
+
return { status: 'pass', reason: `All ${checked} checklist item(s) complete` };
|
|
125
|
+
}
|
|
126
|
+
checkBlockers(scope) {
|
|
127
|
+
const blockedBy = scope.blocked_by ?? [];
|
|
128
|
+
if (blockedBy.length === 0) {
|
|
129
|
+
return { status: 'pass', reason: 'No blockers' };
|
|
130
|
+
}
|
|
131
|
+
const unresolved = [];
|
|
132
|
+
for (const blockerId of blockedBy) {
|
|
133
|
+
const blocker = this.scopeService.getById(blockerId);
|
|
134
|
+
if (blocker && !this.engine.isTerminalStatus(blocker.status)) {
|
|
135
|
+
unresolved.push(blockerId);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (unresolved.length === 0) {
|
|
139
|
+
return { status: 'pass', reason: `All ${blockedBy.length} blocker(s) resolved` };
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
status: 'fail',
|
|
143
|
+
reason: `Blocked by unresolved scope(s): ${unresolved.join(', ')}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
checkDependencies(scope) {
|
|
147
|
+
// Same check as blockers — dependency-check and blocker-check serve similar roles
|
|
148
|
+
return this.checkBlockers(scope);
|
|
149
|
+
}
|
|
150
|
+
checkScopeStructure(scope) {
|
|
151
|
+
if (!scope.title || scope.title.trim() === '') {
|
|
152
|
+
return { status: 'fail', reason: 'Scope has no title' };
|
|
153
|
+
}
|
|
154
|
+
if (!scope.raw_content || scope.raw_content.trim() === '') {
|
|
155
|
+
return { status: 'fail', reason: 'Scope has no content body' };
|
|
156
|
+
}
|
|
157
|
+
return { status: 'pass', reason: 'Scope structure valid' };
|
|
158
|
+
}
|
|
159
|
+
getGatesForScope(scopeId) {
|
|
160
|
+
const scoped = this.gateService.getLatestForScope(scopeId);
|
|
161
|
+
if (scoped.length > 0)
|
|
162
|
+
return scoped;
|
|
163
|
+
// Fall back to global latest run if no scope-specific gates exist
|
|
164
|
+
return this.gateService.getLatestRun();
|
|
165
|
+
}
|
|
166
|
+
computeBlockers(hooks, scope) {
|
|
167
|
+
const blockers = [];
|
|
168
|
+
// Only guards (blockers) actually prevent transitions
|
|
169
|
+
for (const hook of hooks) {
|
|
170
|
+
if (hook.enforcement === 'blocker' && hook.status === 'fail') {
|
|
171
|
+
blockers.push(`${hook.label}: ${hook.reason}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Check for unresolved scope blockers
|
|
175
|
+
const blockedBy = scope.blocked_by ?? [];
|
|
176
|
+
for (const blockerId of blockedBy) {
|
|
177
|
+
const blocker = this.scopeService.getById(blockerId);
|
|
178
|
+
if (blocker && !this.engine.isTerminalStatus(blocker.status)) {
|
|
179
|
+
blockers.push(`Blocked by scope ${blockerId} (${blocker.status})`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return blockers;
|
|
183
|
+
}
|
|
184
|
+
}
|