orbital-command 0.1.4 → 0.3.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/bin/orbital.js +676 -53
- package/dist/assets/PrimitivesConfig-CrmQXYh4.js +32 -0
- package/dist/assets/QualityGates-BbasOsF3.js +21 -0
- package/dist/assets/SessionTimeline-CGeJsVvy.js +1 -0
- package/dist/assets/Settings-oiM496mc.js +12 -0
- package/dist/assets/SourceControl-B1fP2nJL.js +41 -0
- package/dist/assets/WorkflowVisualizer-CWLYf-f0.js +74 -0
- package/dist/assets/arrow-down-CPy85_J6.js +6 -0
- package/dist/assets/charts-DbDg0Psc.js +68 -0
- package/dist/assets/circle-x-Cwz6ZQDV.js +6 -0
- package/dist/assets/file-text-C46Xr65c.js +6 -0
- package/dist/assets/formatDistanceToNow-BMqsSP44.js +1 -0
- package/dist/assets/globe-Cn2yNZUD.js +6 -0
- package/dist/assets/index-Aj4sV8Al.css +1 -0
- package/dist/assets/index-Bc9dK3MW.js +354 -0
- package/dist/assets/key-OPaNTWJ5.js +6 -0
- package/dist/assets/minus-GMsbpKym.js +6 -0
- package/dist/assets/shield-DwAFkDYI.js +6 -0
- package/dist/assets/ui-BmsSg9jU.js +53 -0
- package/dist/assets/useWorkflowEditor-BJkTX_NR.js +16 -0
- package/dist/assets/{vendor-Dzv9lrRc.js → vendor-Bqt8AJn2.js} +1 -1
- package/dist/assets/zap-DfbUoOty.js +11 -0
- package/dist/favicon.svg +1 -0
- package/dist/index.html +6 -5
- package/dist/server/server/__tests__/data-routes.test.js +124 -0
- package/dist/server/server/__tests__/helpers/db.js +17 -0
- package/dist/server/server/__tests__/helpers/mock-emitter.js +8 -0
- package/dist/server/server/__tests__/scope-routes.test.js +137 -0
- package/dist/server/server/__tests__/sprint-routes.test.js +102 -0
- package/dist/server/server/__tests__/workflow-routes.test.js +107 -0
- package/dist/server/server/config-migrator.js +138 -0
- package/dist/server/server/config.js +17 -2
- package/dist/server/server/database.js +27 -12
- package/dist/server/server/global-config.js +143 -0
- package/dist/server/server/index.js +882 -252
- package/dist/server/server/init.js +579 -194
- package/dist/server/server/launch.js +29 -0
- package/dist/server/server/manifest-types.js +8 -0
- package/dist/server/server/manifest.js +454 -0
- package/dist/server/server/migrate-legacy.js +229 -0
- package/dist/server/server/parsers/event-parser.test.js +117 -0
- package/dist/server/server/parsers/scope-parser.js +74 -28
- package/dist/server/server/parsers/scope-parser.test.js +230 -0
- package/dist/server/server/project-context.js +255 -0
- package/dist/server/server/project-emitter.js +41 -0
- package/dist/server/server/project-manager.js +297 -0
- package/dist/server/server/routes/config-routes.js +1 -3
- package/dist/server/server/routes/data-routes.js +22 -110
- package/dist/server/server/routes/dispatch-routes.js +15 -9
- package/dist/server/server/routes/git-routes.js +74 -0
- package/dist/server/server/routes/manifest-routes.js +319 -0
- package/dist/server/server/routes/scope-routes.js +37 -23
- package/dist/server/server/routes/sync-routes.js +134 -0
- package/dist/server/server/routes/version-routes.js +1 -15
- package/dist/server/server/routes/workflow-routes.js +9 -3
- package/dist/server/server/schema.js +2 -0
- package/dist/server/server/services/batch-orchestrator.js +26 -16
- package/dist/server/server/services/claude-session-service.js +17 -14
- package/dist/server/server/services/deploy-service.test.js +119 -0
- package/dist/server/server/services/event-service.js +64 -1
- package/dist/server/server/services/event-service.test.js +191 -0
- package/dist/server/server/services/gate-service.test.js +105 -0
- package/dist/server/server/services/git-service.js +108 -4
- package/dist/server/server/services/github-service.js +110 -2
- package/dist/server/server/services/readiness-service.test.js +190 -0
- package/dist/server/server/services/scope-cache.js +5 -1
- package/dist/server/server/services/scope-cache.test.js +142 -0
- package/dist/server/server/services/scope-service.js +217 -126
- package/dist/server/server/services/scope-service.test.js +137 -0
- package/dist/server/server/services/sprint-orchestrator.js +7 -6
- package/dist/server/server/services/sprint-service.js +21 -1
- package/dist/server/server/services/sprint-service.test.js +238 -0
- package/dist/server/server/services/sync-service.js +434 -0
- package/dist/server/server/services/sync-types.js +2 -0
- package/dist/server/server/services/telemetry-service.js +143 -0
- package/dist/server/server/services/workflow-service.js +26 -5
- package/dist/server/server/services/workflow-service.test.js +159 -0
- package/dist/server/server/settings-sync.js +284 -0
- package/dist/server/server/update-planner.js +279 -0
- package/dist/server/server/utils/cc-hooks-parser.js +3 -0
- package/dist/server/server/utils/cc-hooks-parser.test.js +86 -0
- package/dist/server/server/utils/dispatch-utils.js +77 -20
- package/dist/server/server/utils/dispatch-utils.test.js +182 -0
- package/dist/server/server/utils/logger.js +37 -3
- package/dist/server/server/utils/package-info.js +30 -0
- package/dist/server/server/utils/route-helpers.js +10 -0
- package/dist/server/server/utils/terminal-launcher.js +79 -25
- package/dist/server/server/utils/worktree-manager.js +13 -4
- package/dist/server/server/validator.js +230 -0
- package/dist/server/server/watchers/global-watcher.js +63 -0
- package/dist/server/server/watchers/scope-watcher.js +27 -12
- package/dist/server/server/wizard/config-editor.js +237 -0
- package/dist/server/server/wizard/detect.js +96 -0
- package/dist/server/server/wizard/doctor.js +115 -0
- package/dist/server/server/wizard/index.js +155 -0
- package/dist/server/server/wizard/phases/confirm.js +39 -0
- package/dist/server/server/wizard/phases/project-setup.js +90 -0
- package/dist/server/server/wizard/phases/setup-wizard.js +66 -0
- package/dist/server/server/wizard/phases/welcome.js +35 -0
- package/dist/server/server/wizard/phases/workflow-setup.js +22 -0
- package/dist/server/server/wizard/types.js +29 -0
- package/dist/server/server/wizard/ui.js +74 -0
- package/dist/server/shared/__fixtures__/workflow-configs.js +75 -0
- package/dist/server/shared/default-workflow.json +65 -0
- package/dist/server/shared/onboarding-tour.test.js +81 -0
- package/dist/server/shared/project-colors.js +24 -0
- package/dist/server/shared/workflow-config.test.js +84 -0
- package/dist/server/shared/workflow-engine.test.js +302 -0
- package/dist/server/shared/workflow-normalizer.js +101 -0
- package/dist/server/shared/workflow-normalizer.test.js +100 -0
- package/dist/server/src/components/onboarding/tour-steps.js +84 -0
- package/package.json +20 -15
- package/schemas/orbital.config.schema.json +16 -1
- package/scripts/postinstall.js +55 -7
- package/server/__tests__/data-routes.test.ts +149 -0
- package/server/__tests__/helpers/db.ts +19 -0
- package/server/__tests__/helpers/mock-emitter.ts +10 -0
- package/server/__tests__/scope-routes.test.ts +157 -0
- package/server/__tests__/sprint-routes.test.ts +118 -0
- package/server/__tests__/workflow-routes.test.ts +120 -0
- package/server/config-migrator.ts +163 -0
- package/server/config.ts +26 -2
- package/server/database.ts +35 -18
- package/server/global-config.ts +200 -0
- package/server/index.ts +975 -287
- package/server/init.ts +625 -182
- package/server/launch.ts +32 -0
- package/server/manifest-types.ts +145 -0
- package/server/manifest.ts +494 -0
- package/server/migrate-legacy.ts +290 -0
- package/server/parsers/event-parser.test.ts +135 -0
- package/server/parsers/scope-parser.test.ts +270 -0
- package/server/parsers/scope-parser.ts +79 -31
- package/server/project-context.ts +309 -0
- package/server/project-emitter.ts +50 -0
- package/server/project-manager.ts +369 -0
- package/server/routes/config-routes.ts +3 -5
- package/server/routes/data-routes.ts +28 -141
- package/server/routes/dispatch-routes.ts +19 -11
- package/server/routes/git-routes.ts +77 -0
- package/server/routes/manifest-routes.ts +388 -0
- package/server/routes/scope-routes.ts +29 -25
- package/server/routes/sync-routes.ts +175 -0
- package/server/routes/version-routes.ts +1 -16
- package/server/routes/workflow-routes.ts +9 -3
- package/server/schema.ts +2 -0
- package/server/services/batch-orchestrator.ts +24 -16
- package/server/services/claude-session-service.ts +16 -14
- package/server/services/deploy-service.test.ts +145 -0
- package/server/services/deploy-service.ts +2 -2
- package/server/services/event-service.test.ts +242 -0
- package/server/services/event-service.ts +92 -3
- package/server/services/gate-service.test.ts +131 -0
- package/server/services/gate-service.ts +2 -2
- package/server/services/git-service.ts +137 -4
- package/server/services/github-service.ts +120 -2
- package/server/services/readiness-service.test.ts +217 -0
- package/server/services/scope-cache.test.ts +167 -0
- package/server/services/scope-cache.ts +4 -1
- package/server/services/scope-service.test.ts +169 -0
- package/server/services/scope-service.ts +220 -126
- package/server/services/sprint-orchestrator.ts +7 -7
- package/server/services/sprint-service.test.ts +271 -0
- package/server/services/sprint-service.ts +27 -3
- package/server/services/sync-service.ts +482 -0
- package/server/services/sync-types.ts +77 -0
- package/server/services/telemetry-service.ts +195 -0
- package/server/services/workflow-service.test.ts +190 -0
- package/server/services/workflow-service.ts +29 -9
- package/server/settings-sync.ts +359 -0
- package/server/update-planner.ts +346 -0
- package/server/utils/cc-hooks-parser.test.ts +96 -0
- package/server/utils/cc-hooks-parser.ts +4 -0
- package/server/utils/dispatch-utils.test.ts +245 -0
- package/server/utils/dispatch-utils.ts +97 -27
- package/server/utils/logger.ts +40 -3
- package/server/utils/package-info.ts +32 -0
- package/server/utils/route-helpers.ts +12 -0
- package/server/utils/terminal-launcher.ts +85 -25
- package/server/utils/worktree-manager.ts +9 -4
- package/server/validator.ts +270 -0
- package/server/watchers/global-watcher.ts +77 -0
- package/server/watchers/scope-watcher.ts +21 -9
- package/server/wizard/config-editor.ts +248 -0
- package/server/wizard/detect.ts +104 -0
- package/server/wizard/doctor.ts +114 -0
- package/server/wizard/index.ts +187 -0
- package/server/wizard/phases/confirm.ts +45 -0
- package/server/wizard/phases/project-setup.ts +106 -0
- package/server/wizard/phases/setup-wizard.ts +78 -0
- package/server/wizard/phases/welcome.ts +43 -0
- package/server/wizard/phases/workflow-setup.ts +28 -0
- package/server/wizard/types.ts +56 -0
- package/server/wizard/ui.ts +93 -0
- package/shared/__fixtures__/workflow-configs.ts +80 -0
- package/shared/default-workflow.json +65 -0
- package/shared/onboarding-tour.test.ts +94 -0
- package/shared/project-colors.ts +24 -0
- package/shared/workflow-config.test.ts +111 -0
- package/shared/workflow-config.ts +7 -0
- package/shared/workflow-engine.test.ts +388 -0
- package/shared/workflow-normalizer.test.ts +119 -0
- package/shared/workflow-normalizer.ts +118 -0
- package/templates/hooks/end-session.sh +3 -1
- package/templates/hooks/orbital-emit.sh +2 -2
- package/templates/hooks/orbital-report-deploy.sh +4 -4
- package/templates/hooks/orbital-report-gates.sh +4 -4
- package/templates/hooks/orbital-scope-update.sh +1 -1
- package/templates/hooks/scope-create-cleanup.sh +2 -2
- package/templates/hooks/scope-create-gate.sh +0 -1
- package/templates/hooks/scope-helpers.sh +18 -0
- package/templates/hooks/scope-prepare.sh +66 -11
- package/templates/migrations/renames.json +1 -0
- package/templates/orbital.config.json +7 -2
- package/templates/settings-hooks.json +1 -1
- package/templates/skills/git-commit/SKILL.md +9 -4
- package/templates/skills/git-dev/SKILL.md +8 -3
- package/templates/skills/git-main/SKILL.md +8 -2
- package/templates/skills/git-production/SKILL.md +6 -2
- package/templates/skills/git-staging/SKILL.md +8 -3
- package/templates/skills/scope-create/SKILL.md +17 -3
- package/templates/skills/scope-fix-review/SKILL.md +6 -3
- package/templates/skills/scope-implement/SKILL.md +4 -1
- package/templates/skills/scope-post-review/SKILL.md +63 -5
- package/templates/skills/scope-pre-review/SKILL.md +5 -2
- package/templates/skills/scope-verify/SKILL.md +5 -3
- package/templates/skills/test-code-review/SKILL.md +41 -33
- package/templates/skills/test-scaffold/SKILL.md +222 -0
- package/dist/assets/WorkflowVisualizer-BZ21PIIF.js +0 -84
- package/dist/assets/charts-D__PA1zp.js +0 -72
- package/dist/assets/index-D1G6i0nS.css +0 -1
- package/dist/assets/index-DpItvKpf.js +0 -419
- package/dist/assets/ui-BvF022GT.js +0 -53
- package/index.html +0 -15
- package/postcss.config.js +0 -6
- package/src/App.tsx +0 -33
- package/src/components/AgentBadge.tsx +0 -40
- package/src/components/BatchPreflightModal.tsx +0 -115
- package/src/components/CardDisplayToggle.tsx +0 -74
- package/src/components/ColumnHeaderActions.tsx +0 -55
- package/src/components/ColumnMenu.tsx +0 -99
- package/src/components/DeployHistory.tsx +0 -141
- package/src/components/DispatchModal.tsx +0 -164
- package/src/components/DispatchPopover.tsx +0 -139
- package/src/components/DragOverlay.tsx +0 -25
- package/src/components/DriftSidebar.tsx +0 -140
- package/src/components/EnvironmentStrip.tsx +0 -88
- package/src/components/ErrorBoundary.tsx +0 -62
- package/src/components/FilterChip.tsx +0 -105
- package/src/components/GateIndicator.tsx +0 -33
- package/src/components/IdeaDetailModal.tsx +0 -190
- package/src/components/IdeaFormDialog.tsx +0 -113
- package/src/components/KanbanColumn.tsx +0 -201
- package/src/components/MarkdownRenderer.tsx +0 -114
- package/src/components/NeonGrid.tsx +0 -128
- package/src/components/PromotionQueue.tsx +0 -89
- package/src/components/ScopeCard.tsx +0 -234
- package/src/components/ScopeDetailModal.tsx +0 -255
- package/src/components/ScopeFilterBar.tsx +0 -152
- package/src/components/SearchInput.tsx +0 -102
- package/src/components/SessionPanel.tsx +0 -335
- package/src/components/SprintContainer.tsx +0 -303
- package/src/components/SprintDependencyDialog.tsx +0 -78
- package/src/components/SprintPreflightModal.tsx +0 -138
- package/src/components/StatusBar.tsx +0 -168
- package/src/components/SwimCell.tsx +0 -67
- package/src/components/SwimLaneRow.tsx +0 -94
- package/src/components/SwimlaneBoardView.tsx +0 -108
- package/src/components/VersionBadge.tsx +0 -139
- package/src/components/ViewModeSelector.tsx +0 -114
- package/src/components/config/AgentChip.tsx +0 -53
- package/src/components/config/AgentCreateDialog.tsx +0 -321
- package/src/components/config/AgentEditor.tsx +0 -175
- package/src/components/config/DirectoryTree.tsx +0 -582
- package/src/components/config/FileEditor.tsx +0 -550
- package/src/components/config/HookChip.tsx +0 -50
- package/src/components/config/StageCard.tsx +0 -198
- package/src/components/config/TransitionZone.tsx +0 -173
- package/src/components/config/UnifiedWorkflowPipeline.tsx +0 -216
- package/src/components/config/WorkflowPipeline.tsx +0 -161
- package/src/components/source-control/BranchList.tsx +0 -93
- package/src/components/source-control/BranchPanel.tsx +0 -105
- package/src/components/source-control/CommitLog.tsx +0 -100
- package/src/components/source-control/CommitRow.tsx +0 -47
- package/src/components/source-control/GitHubPanel.tsx +0 -110
- package/src/components/source-control/GitHubSetupGuide.tsx +0 -52
- package/src/components/source-control/GitOverviewBar.tsx +0 -101
- package/src/components/source-control/PullRequestList.tsx +0 -69
- package/src/components/source-control/WorktreeList.tsx +0 -80
- package/src/components/ui/badge.tsx +0 -41
- package/src/components/ui/button.tsx +0 -55
- package/src/components/ui/card.tsx +0 -78
- package/src/components/ui/dialog.tsx +0 -94
- package/src/components/ui/popover.tsx +0 -33
- package/src/components/ui/scroll-area.tsx +0 -54
- package/src/components/ui/separator.tsx +0 -28
- package/src/components/ui/tabs.tsx +0 -52
- package/src/components/ui/toggle-switch.tsx +0 -35
- package/src/components/ui/tooltip.tsx +0 -27
- package/src/components/workflow/AddEdgeDialog.tsx +0 -217
- package/src/components/workflow/AddListDialog.tsx +0 -201
- package/src/components/workflow/ChecklistEditor.tsx +0 -239
- package/src/components/workflow/CommandPrefixManager.tsx +0 -118
- package/src/components/workflow/ConfigSettingsPanel.tsx +0 -189
- package/src/components/workflow/DirectionSelector.tsx +0 -133
- package/src/components/workflow/DispatchConfigPanel.tsx +0 -180
- package/src/components/workflow/EdgeDetailPanel.tsx +0 -236
- package/src/components/workflow/EdgePropertyEditor.tsx +0 -251
- package/src/components/workflow/EditToolbar.tsx +0 -138
- package/src/components/workflow/HookDetailPanel.tsx +0 -250
- package/src/components/workflow/HookExecutionLog.tsx +0 -24
- package/src/components/workflow/HookSourceModal.tsx +0 -129
- package/src/components/workflow/HooksDashboard.tsx +0 -363
- package/src/components/workflow/ListPropertyEditor.tsx +0 -251
- package/src/components/workflow/MigrationPreviewDialog.tsx +0 -237
- package/src/components/workflow/MovementRulesPanel.tsx +0 -188
- package/src/components/workflow/NodeDetailPanel.tsx +0 -245
- package/src/components/workflow/PresetSelector.tsx +0 -414
- package/src/components/workflow/SkillCommandBuilder.tsx +0 -174
- package/src/components/workflow/WorkflowEdgeComponent.tsx +0 -145
- package/src/components/workflow/WorkflowNode.tsx +0 -147
- package/src/components/workflow/graphLayout.ts +0 -186
- package/src/components/workflow/mergeHooks.ts +0 -85
- package/src/components/workflow/useEditHistory.ts +0 -88
- package/src/components/workflow/useWorkflowEditor.ts +0 -262
- package/src/components/workflow/validateConfig.ts +0 -70
- package/src/hooks/useActiveDispatches.ts +0 -198
- package/src/hooks/useBoardSettings.ts +0 -170
- package/src/hooks/useCardDisplay.ts +0 -57
- package/src/hooks/useCcHooks.ts +0 -24
- package/src/hooks/useConfigTree.ts +0 -51
- package/src/hooks/useEnforcementRules.ts +0 -46
- package/src/hooks/useEvents.ts +0 -59
- package/src/hooks/useFileEditor.ts +0 -165
- package/src/hooks/useGates.ts +0 -57
- package/src/hooks/useIdeaActions.ts +0 -53
- package/src/hooks/useKanbanDnd.ts +0 -410
- package/src/hooks/useOrbitalConfig.ts +0 -54
- package/src/hooks/usePipeline.ts +0 -47
- package/src/hooks/usePipelineData.ts +0 -338
- package/src/hooks/useReconnect.ts +0 -25
- package/src/hooks/useScopeFilters.ts +0 -125
- package/src/hooks/useScopeSessions.ts +0 -44
- package/src/hooks/useScopes.ts +0 -67
- package/src/hooks/useSearch.ts +0 -67
- package/src/hooks/useSettings.tsx +0 -187
- package/src/hooks/useSocket.ts +0 -25
- package/src/hooks/useSourceControl.ts +0 -105
- package/src/hooks/useSprintPreflight.ts +0 -55
- package/src/hooks/useSprints.ts +0 -154
- package/src/hooks/useStatusBarHighlight.ts +0 -18
- package/src/hooks/useSwimlaneBoardSettings.ts +0 -104
- package/src/hooks/useTheme.ts +0 -9
- package/src/hooks/useTransitionReadiness.ts +0 -53
- package/src/hooks/useVersion.ts +0 -155
- package/src/hooks/useViolations.ts +0 -65
- package/src/hooks/useWorkflow.tsx +0 -125
- package/src/hooks/useZoomModifier.ts +0 -19
- package/src/index.css +0 -797
- package/src/layouts/DashboardLayout.tsx +0 -113
- package/src/lib/collisionDetection.ts +0 -20
- package/src/lib/scope-fields.ts +0 -61
- package/src/lib/swimlane.ts +0 -146
- package/src/lib/utils.ts +0 -15
- package/src/main.tsx +0 -19
- package/src/socket.ts +0 -11
- package/src/types/index.ts +0 -497
- package/src/views/AgentFeed.tsx +0 -339
- package/src/views/DeployPipeline.tsx +0 -59
- package/src/views/EnforcementView.tsx +0 -378
- package/src/views/PrimitivesConfig.tsx +0 -500
- package/src/views/QualityGates.tsx +0 -1012
- package/src/views/ScopeBoard.tsx +0 -454
- package/src/views/SessionTimeline.tsx +0 -516
- package/src/views/Settings.tsx +0 -183
- package/src/views/SourceControl.tsx +0 -95
- package/src/views/WorkflowVisualizer.tsx +0 -382
- package/tailwind.config.js +0 -161
- package/tsconfig.json +0 -25
- package/vite.config.ts +0 -49
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { execFile as execFileCb } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import { listWorktrees } from '../utils/worktree-manager.js';
|
|
4
|
+
import { createLogger } from '../utils/logger.js';
|
|
4
5
|
import type { ScopeCache } from './scope-cache.js';
|
|
5
6
|
|
|
6
7
|
const execFile = promisify(execFileCb);
|
|
8
|
+
const log = createLogger('git');
|
|
7
9
|
|
|
8
10
|
// ─── Types ──────────────────────────────────────────────────
|
|
9
11
|
|
|
@@ -182,7 +184,8 @@ export class GitService {
|
|
|
182
184
|
let raw: string;
|
|
183
185
|
try {
|
|
184
186
|
raw = await this.git(args);
|
|
185
|
-
} catch {
|
|
187
|
+
} catch (err) {
|
|
188
|
+
log.debug('Commits query failed', { branch: branch ?? 'all', error: String(err) });
|
|
186
189
|
return [];
|
|
187
190
|
}
|
|
188
191
|
|
|
@@ -241,7 +244,8 @@ export class GitService {
|
|
|
241
244
|
'branch', '-a',
|
|
242
245
|
'--format=%(HEAD)|%(refname:short)|%(objectname:short)|%(committerdate:iso-strict)|%(subject)',
|
|
243
246
|
]);
|
|
244
|
-
} catch {
|
|
247
|
+
} catch (err) {
|
|
248
|
+
log.debug('Branch listing failed', { error: String(err) });
|
|
245
249
|
return [];
|
|
246
250
|
}
|
|
247
251
|
|
|
@@ -304,7 +308,8 @@ export class GitService {
|
|
|
304
308
|
let wts: Array<{ path: string; branch: string; scopeId: number }>;
|
|
305
309
|
try {
|
|
306
310
|
wts = await listWorktrees(this.projectRoot);
|
|
307
|
-
} catch {
|
|
311
|
+
} catch (err) {
|
|
312
|
+
log.warn('Failed to list worktrees', { error: String(err) });
|
|
308
313
|
return [];
|
|
309
314
|
}
|
|
310
315
|
|
|
@@ -366,7 +371,8 @@ export class GitService {
|
|
|
366
371
|
return { sha, date, message: rest.slice(0, -1).join('|'), author: rest[rest.length - 1] };
|
|
367
372
|
});
|
|
368
373
|
pairs.push({ from, to, count: commits.length, commits });
|
|
369
|
-
} catch {
|
|
374
|
+
} catch (err) {
|
|
375
|
+
log.debug('Drift query failed', { from, to, error: String(err) });
|
|
370
376
|
pairs.push({ from, to, count: 0, commits: [] });
|
|
371
377
|
}
|
|
372
378
|
}
|
|
@@ -375,6 +381,83 @@ export class GitService {
|
|
|
375
381
|
return pairs;
|
|
376
382
|
}
|
|
377
383
|
|
|
384
|
+
// ─── Activity Series ────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
async getActivitySeries(days: number = 30): Promise<Array<{ date: string; count: number }>> {
|
|
387
|
+
const cacheKey = `activity:${days}`;
|
|
388
|
+
const hit = cached<Array<{ date: string; count: number }>>(this.cache as Map<string, CacheEntry<Array<{ date: string; count: number }>>>, cacheKey);
|
|
389
|
+
if (hit) return hit;
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
const raw = await this.git(['log', `--since=${days}.days.ago`, '--format=%aI', '--all']);
|
|
393
|
+
const counts = new Map<string, number>();
|
|
394
|
+
|
|
395
|
+
// Initialize all days to 0
|
|
396
|
+
const now = new Date();
|
|
397
|
+
for (let i = 0; i < days; i++) {
|
|
398
|
+
const d = new Date(now);
|
|
399
|
+
d.setDate(d.getDate() - i);
|
|
400
|
+
counts.set(d.toISOString().slice(0, 10), 0);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
for (const line of raw.trim().split('\n')) {
|
|
404
|
+
if (!line) continue;
|
|
405
|
+
const dateKey = line.slice(0, 10);
|
|
406
|
+
counts.set(dateKey, (counts.get(dateKey) ?? 0) + 1);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const series = [...counts.entries()]
|
|
410
|
+
.map(([date, count]) => ({ date, count }))
|
|
411
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
412
|
+
|
|
413
|
+
setCache(this.cache as Map<string, CacheEntry<Array<{ date: string; count: number }>>>, cacheKey, series);
|
|
414
|
+
return series;
|
|
415
|
+
} catch (err) {
|
|
416
|
+
log.debug('Activity series query failed', { days, error: String(err) });
|
|
417
|
+
return [];
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ─── Health Metrics ────────────────────────────────────────
|
|
422
|
+
|
|
423
|
+
async getHealthMetrics(githubPrAges?: number[]): Promise<{
|
|
424
|
+
commitsPerWeek: number;
|
|
425
|
+
avgPrAgeDays: number;
|
|
426
|
+
staleBranchCount: number;
|
|
427
|
+
driftSeverity: 'clean' | 'low' | 'moderate' | 'high';
|
|
428
|
+
grade: 'A' | 'B' | 'C' | 'D' | 'F';
|
|
429
|
+
}> {
|
|
430
|
+
// Commits per week
|
|
431
|
+
let commitsPerWeek = 0;
|
|
432
|
+
try {
|
|
433
|
+
const raw = await this.git(['log', '--since=7.days.ago', '--oneline', '--all']);
|
|
434
|
+
commitsPerWeek = raw.trim().split('\n').filter(Boolean).length;
|
|
435
|
+
} catch { /* ok */ }
|
|
436
|
+
|
|
437
|
+
// Stale branches
|
|
438
|
+
const branches = await this.getBranches();
|
|
439
|
+
const staleBranchCount = branches.filter(b => b.isStale && !b.isRemote).length;
|
|
440
|
+
|
|
441
|
+
// Avg PR age
|
|
442
|
+
let avgPrAgeDays = 0;
|
|
443
|
+
if (githubPrAges && githubPrAges.length > 0) {
|
|
444
|
+
avgPrAgeDays = Math.round(githubPrAges.reduce((a, b) => a + b, 0) / githubPrAges.length);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Drift severity (reuse cached data if available)
|
|
448
|
+
const driftSeverity: 'clean' | 'low' | 'moderate' | 'high' = 'clean';
|
|
449
|
+
|
|
450
|
+
// Grade computation: start at 100
|
|
451
|
+
let score = 100;
|
|
452
|
+
score -= Math.min(30, staleBranchCount * 5);
|
|
453
|
+
if (commitsPerWeek < 5) score -= 10;
|
|
454
|
+
if (avgPrAgeDays > 3) score -= Math.min(25, (avgPrAgeDays - 3) * 5);
|
|
455
|
+
|
|
456
|
+
const grade = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : score >= 60 ? 'D' : 'F';
|
|
457
|
+
|
|
458
|
+
return { commitsPerWeek, avgPrAgeDays, staleBranchCount, driftSeverity, grade };
|
|
459
|
+
}
|
|
460
|
+
|
|
378
461
|
// ─── Git Status Polling ────────────────────────────────────
|
|
379
462
|
|
|
380
463
|
async getStatusHash(): Promise<string> {
|
|
@@ -385,6 +468,56 @@ export class GitService {
|
|
|
385
468
|
return `${head.trim()}:${dirty.trim().length > 0 ? 'dirty' : 'clean'}`;
|
|
386
469
|
}
|
|
387
470
|
|
|
471
|
+
// ─── Pipeline Drift ─────────────────────────────────────────
|
|
472
|
+
|
|
473
|
+
async getPipelineDrift(): Promise<{
|
|
474
|
+
devToStaging: { count: number; commits: Array<{ sha: string; message: string; author: string; date: string }>; oldestDate: string | null };
|
|
475
|
+
stagingToMain: { count: number; commits: Array<{ sha: string; message: string; author: string; date: string }>; oldestDate: string | null };
|
|
476
|
+
heads: { dev: { sha: string; date: string; message: string }; staging: { sha: string; date: string; message: string }; main: { sha: string; date: string; message: string } };
|
|
477
|
+
}> {
|
|
478
|
+
type DriftCommit = { sha: string; message: string; author: string; date: string };
|
|
479
|
+
type BranchHead = { sha: string; date: string; message: string };
|
|
480
|
+
|
|
481
|
+
const cacheKey = 'pipeline-drift';
|
|
482
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
483
|
+
const hit = cached<any>(this.cache as Map<string, CacheEntry<any>>, cacheKey);
|
|
484
|
+
if (hit) return hit;
|
|
485
|
+
|
|
486
|
+
function parseDriftCommits(raw: string): DriftCommit[] {
|
|
487
|
+
if (!raw) return [];
|
|
488
|
+
return raw.split('\n').map((line) => {
|
|
489
|
+
const [sha, date, message, author] = line.split('|');
|
|
490
|
+
return { sha, date, message: message ?? '', author: author ?? '' };
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function parseHead(raw: string): BranchHead {
|
|
495
|
+
const [sha, date, message] = raw.split('|');
|
|
496
|
+
return { sha: sha ?? '', date: date ?? '', message: message ?? '' };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const [devToStagingRaw, stagingToMainRaw, devHead, stagingHead, mainHead] =
|
|
500
|
+
await Promise.all([
|
|
501
|
+
this.git(['log', 'origin/dev', '--not', 'origin/staging', '--reverse', '--format=%H|%aI|%s|%an']).catch(() => ''),
|
|
502
|
+
this.git(['log', 'origin/staging', '--not', 'origin/main', '--reverse', '--format=%H|%aI|%s|%an']).catch(() => ''),
|
|
503
|
+
this.git(['log', 'origin/dev', '-1', '--format=%H|%aI|%s']).catch(() => ''),
|
|
504
|
+
this.git(['log', 'origin/staging', '-1', '--format=%H|%aI|%s']).catch(() => ''),
|
|
505
|
+
this.git(['log', 'origin/main', '-1', '--format=%H|%aI|%s']).catch(() => ''),
|
|
506
|
+
]);
|
|
507
|
+
|
|
508
|
+
const devToStaging = parseDriftCommits(devToStagingRaw);
|
|
509
|
+
const stagingToMain = parseDriftCommits(stagingToMainRaw);
|
|
510
|
+
|
|
511
|
+
const data = {
|
|
512
|
+
devToStaging: { count: devToStaging.length, commits: devToStaging, oldestDate: devToStaging[0]?.date ?? null },
|
|
513
|
+
stagingToMain: { count: stagingToMain.length, commits: stagingToMain, oldestDate: stagingToMain[0]?.date ?? null },
|
|
514
|
+
heads: { dev: parseHead(devHead), staging: parseHead(stagingHead), main: parseHead(mainHead) },
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
setCache(this.cache as Map<string, CacheEntry<typeof data>>, cacheKey, data);
|
|
518
|
+
return data;
|
|
519
|
+
}
|
|
520
|
+
|
|
388
521
|
clearCache(): void {
|
|
389
522
|
this.cache.clear();
|
|
390
523
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFile as execFileCb } from 'child_process';
|
|
1
|
+
import { execFile as execFileCb, spawn } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
|
|
4
4
|
// Uses execFile (not exec) — safe against shell injection
|
|
@@ -144,7 +144,7 @@ export class GitHubService {
|
|
|
144
144
|
try {
|
|
145
145
|
const raw = await this.gh([
|
|
146
146
|
'pr', 'list', '--state', 'open', '--json',
|
|
147
|
-
'number,title,author,headRefName,baseRefName,state,url,createdAt',
|
|
147
|
+
'number,title,author,headRefName,baseRefName,state,url,createdAt,updatedAt,reviewDecision',
|
|
148
148
|
'--limit', '30',
|
|
149
149
|
]);
|
|
150
150
|
const parsed = JSON.parse(raw);
|
|
@@ -171,6 +171,8 @@ export class GitHubService {
|
|
|
171
171
|
url: String(pr.url ?? ''),
|
|
172
172
|
createdAt: String(pr.createdAt ?? ''),
|
|
173
173
|
scopeIds,
|
|
174
|
+
reviewDecision: (pr.reviewDecision as string) || null,
|
|
175
|
+
lastActivityAt: String(pr.updatedAt ?? pr.createdAt ?? ''),
|
|
174
176
|
};
|
|
175
177
|
});
|
|
176
178
|
|
|
@@ -180,4 +182,120 @@ export class GitHubService {
|
|
|
180
182
|
return [];
|
|
181
183
|
}
|
|
182
184
|
}
|
|
185
|
+
|
|
186
|
+
// ─── Auth Flow ─────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
/** Start OAuth flow via gh CLI — opens browser. Client polls getAuthStatus(). */
|
|
189
|
+
async connectOAuth(): Promise<{ success: boolean; error?: string }> {
|
|
190
|
+
return new Promise((resolve) => {
|
|
191
|
+
// spawn is safe here: no shell, args are hardcoded literals
|
|
192
|
+
const child = spawn('gh', ['auth', 'login', '--web', '--git-protocol', 'https'], {
|
|
193
|
+
cwd: this.projectRoot,
|
|
194
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
let stderr = '';
|
|
198
|
+
child.stderr?.on('data', (data: Buffer) => { stderr += data.toString(); });
|
|
199
|
+
|
|
200
|
+
// Resolve quickly — the gh process runs in background, client polls auth status
|
|
201
|
+
setTimeout(() => {
|
|
202
|
+
resolve({ success: true });
|
|
203
|
+
}, 500);
|
|
204
|
+
|
|
205
|
+
child.on('error', () => {
|
|
206
|
+
resolve({ success: false, error: stderr || 'Failed to start auth flow' });
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Authenticate using a Personal Access Token piped to gh stdin. */
|
|
212
|
+
async connectWithToken(token: string): Promise<{ success: boolean; error?: string }> {
|
|
213
|
+
try {
|
|
214
|
+
// spawn is safe: no shell, args are hardcoded literals, token via stdin (not args)
|
|
215
|
+
const child = spawn('gh', ['auth', 'login', '--with-token'], {
|
|
216
|
+
cwd: this.projectRoot,
|
|
217
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return new Promise((resolve) => {
|
|
221
|
+
let stderr = '';
|
|
222
|
+
child.stderr?.on('data', (data: Buffer) => { stderr += data.toString(); });
|
|
223
|
+
|
|
224
|
+
child.on('close', (code) => {
|
|
225
|
+
this.clearCaches();
|
|
226
|
+
if (code === 0) {
|
|
227
|
+
resolve({ success: true });
|
|
228
|
+
} else {
|
|
229
|
+
resolve({ success: false, error: stderr || 'Authentication failed' });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
child.on('error', () => {
|
|
234
|
+
resolve({ success: false, error: 'Failed to run gh auth' });
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
child.stdin?.write(token);
|
|
238
|
+
child.stdin?.end();
|
|
239
|
+
});
|
|
240
|
+
} catch {
|
|
241
|
+
return { success: false, error: 'Failed to authenticate' };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Lightweight auth check — returns current user if authenticated. */
|
|
246
|
+
async getAuthStatus(): Promise<{ authenticated: boolean; user?: string }> {
|
|
247
|
+
try {
|
|
248
|
+
const whoami = await this.gh(['api', 'user', '--jq', '.login']);
|
|
249
|
+
const user = whoami.trim();
|
|
250
|
+
return user ? { authenticated: true, user } : { authenticated: false };
|
|
251
|
+
} catch {
|
|
252
|
+
return { authenticated: false };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Log out of GitHub CLI. */
|
|
257
|
+
async disconnect(): Promise<{ success: boolean; error?: string }> {
|
|
258
|
+
try {
|
|
259
|
+
// execFile is safe — no shell injection
|
|
260
|
+
await execFile('gh', ['auth', 'logout', '--hostname', 'github.com'], {
|
|
261
|
+
cwd: this.projectRoot,
|
|
262
|
+
timeout: 10_000,
|
|
263
|
+
env: { ...process.env, GH_PROMPT_DISABLED: '1' },
|
|
264
|
+
});
|
|
265
|
+
this.clearCaches();
|
|
266
|
+
return { success: true };
|
|
267
|
+
} catch (err) {
|
|
268
|
+
this.clearCaches();
|
|
269
|
+
return { success: false, error: String(err) };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ─── CI Checks ─────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
/** Fetch GitHub Actions check runs for a commit ref. */
|
|
276
|
+
async getCheckRuns(ref: string): Promise<Array<{ name: string; status: string; conclusion: string | null; url: string }>> {
|
|
277
|
+
// Validate ref to prevent path traversal — only allow hex SHA and branch-like names
|
|
278
|
+
if (!/^[a-zA-Z0-9._/-]+$/.test(ref)) return [];
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const raw = await this.gh([
|
|
282
|
+
'api', `repos/{owner}/{repo}/commits/${ref}/check-runs`,
|
|
283
|
+
'--jq', '.check_runs | map({name, status, conclusion, html_url})',
|
|
284
|
+
]);
|
|
285
|
+
const parsed = JSON.parse(raw);
|
|
286
|
+
return (parsed as Array<Record<string, unknown>>).map(c => ({
|
|
287
|
+
name: String(c.name ?? ''),
|
|
288
|
+
status: String(c.status ?? 'queued'),
|
|
289
|
+
conclusion: c.conclusion ? String(c.conclusion) : null,
|
|
290
|
+
url: String(c.html_url ?? ''),
|
|
291
|
+
}));
|
|
292
|
+
} catch {
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private clearCaches(): void {
|
|
298
|
+
this.statusCache = null;
|
|
299
|
+
this.prCache = null;
|
|
300
|
+
}
|
|
183
301
|
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { ReadinessService } from './readiness-service.js';
|
|
3
|
+
import { WorkflowEngine } from '../../shared/workflow-engine.js';
|
|
4
|
+
import { CONFIG_WITH_HOOKS } from '../../shared/__fixtures__/workflow-configs.js';
|
|
5
|
+
import type { ParsedScope } from '../parsers/scope-parser.js';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
function makeScope(overrides: Partial<ParsedScope> & { id: number }): ParsedScope {
|
|
11
|
+
return {
|
|
12
|
+
title: `Scope ${overrides.id}`,
|
|
13
|
+
slug: undefined,
|
|
14
|
+
status: 'backlog',
|
|
15
|
+
priority: null,
|
|
16
|
+
effort_estimate: null,
|
|
17
|
+
category: null,
|
|
18
|
+
tags: [],
|
|
19
|
+
blocked_by: [],
|
|
20
|
+
blocks: [],
|
|
21
|
+
file_path: `/scopes/backlog/${String(overrides.id).padStart(3, '0')}-test.md`,
|
|
22
|
+
created_at: null,
|
|
23
|
+
updated_at: null,
|
|
24
|
+
raw_content: '# Test Scope\nSome content here.',
|
|
25
|
+
sessions: {},
|
|
26
|
+
is_ghost: false,
|
|
27
|
+
favourite: false,
|
|
28
|
+
...overrides,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('ReadinessService', () => {
|
|
33
|
+
let engine: WorkflowEngine;
|
|
34
|
+
let tmpDir: string;
|
|
35
|
+
let service: ReadinessService;
|
|
36
|
+
let mockScopeService: { getById: (id: number) => ParsedScope | undefined };
|
|
37
|
+
let mockGateService: { getLatestForScope: ReturnType<typeof vi.fn>; getLatestRun: ReturnType<typeof vi.fn> };
|
|
38
|
+
let scopes: ParsedScope[];
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
engine = new WorkflowEngine(CONFIG_WITH_HOOKS);
|
|
42
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'readiness-test-'));
|
|
43
|
+
|
|
44
|
+
scopes = [
|
|
45
|
+
makeScope({ id: 1, status: 'backlog', sessions: { implementScope: ['sess-1'] } }),
|
|
46
|
+
makeScope({ id: 2, status: 'active', sessions: { implementScope: ['sess-1'] }, blocked_by: [99] }),
|
|
47
|
+
makeScope({ id: 3, status: 'active', sessions: { implementScope: ['sess-1'] }, raw_content: '# Scope\n- [ ] Task 1\n- [x] Task 2' }),
|
|
48
|
+
makeScope({ id: 99, status: 'shipped' }), // terminal blocker
|
|
49
|
+
makeScope({ id: 100, status: 'backlog' }), // non-terminal blocker
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
mockScopeService = { getById: (id: number) => scopes.find(s => s.id === id) };
|
|
53
|
+
mockGateService = {
|
|
54
|
+
getLatestForScope: vi.fn().mockReturnValue([]),
|
|
55
|
+
getLatestRun: vi.fn().mockReturnValue([]),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
service = new ReadinessService(
|
|
59
|
+
mockScopeService as any,
|
|
60
|
+
mockGateService as any,
|
|
61
|
+
engine,
|
|
62
|
+
tmpDir,
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('getReadiness()', () => {
|
|
71
|
+
it('returns null for unknown scope', () => {
|
|
72
|
+
expect(service.getReadiness(999)).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('returns readiness with transitions for known scope', () => {
|
|
76
|
+
const result = service.getReadiness(1);
|
|
77
|
+
expect(result).not.toBeNull();
|
|
78
|
+
expect(result!.scope_id).toBe(1);
|
|
79
|
+
expect(result!.transitions).toBeDefined();
|
|
80
|
+
expect(result!.transitions.length).toBeGreaterThan(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('includes hook statuses for each transition', () => {
|
|
84
|
+
const result = service.getReadiness(1)!;
|
|
85
|
+
const transition = result.transitions[0];
|
|
86
|
+
expect(transition.hooks).toBeDefined();
|
|
87
|
+
expect(Array.isArray(transition.hooks)).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('session-enforcer hook', () => {
|
|
92
|
+
it('passes when session key exists for target column', () => {
|
|
93
|
+
// Scope 1 is in backlog, transition to active requires implementScope session key
|
|
94
|
+
// Scope 1 has sessions.implementScope = ['sess-1']
|
|
95
|
+
const result = service.getReadiness(1)!;
|
|
96
|
+
const toActive = result.transitions.find(t => t.to === 'active');
|
|
97
|
+
if (toActive) {
|
|
98
|
+
const enforcer = toActive.hooks.find(h => h.id === 'session-enforcer');
|
|
99
|
+
// If session-enforcer is on this edge, it should look at the target session key
|
|
100
|
+
if (enforcer) {
|
|
101
|
+
// The scope has the required session, so it should pass
|
|
102
|
+
expect(['pass', 'unknown']).toContain(enforcer.status);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('blocker-check hook', () => {
|
|
109
|
+
it('passes when blocker is in terminal status', () => {
|
|
110
|
+
// Scope 2 blocked_by [99], scope 99 is shipped (terminal)
|
|
111
|
+
const result = service.getReadiness(2)!;
|
|
112
|
+
const transition = result.transitions.find(t => t.hooks.some(h => h.id === 'blocker-check'));
|
|
113
|
+
if (transition) {
|
|
114
|
+
const blockerHook = transition.hooks.find(h => h.id === 'blocker-check');
|
|
115
|
+
expect(blockerHook?.status).toBe('pass');
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('fails when blocker is not in terminal status', () => {
|
|
120
|
+
// Add a scope blocked by non-terminal scope
|
|
121
|
+
scopes.push(makeScope({ id: 5, status: 'active', blocked_by: [100], sessions: { implementScope: ['s'] } }));
|
|
122
|
+
const result = service.getReadiness(5)!;
|
|
123
|
+
const transition = result.transitions.find(t => t.hooks.some(h => h.id === 'blocker-check'));
|
|
124
|
+
if (transition) {
|
|
125
|
+
const blockerHook = transition.hooks.find(h => h.id === 'blocker-check');
|
|
126
|
+
expect(blockerHook?.status).toBe('fail');
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('review-gate-check hook', () => {
|
|
132
|
+
it('passes when verdict file exists with PASS', () => {
|
|
133
|
+
const verdictDir = path.join(tmpDir, '.claude', 'review-verdicts');
|
|
134
|
+
fs.mkdirSync(verdictDir, { recursive: true });
|
|
135
|
+
fs.writeFileSync(path.join(verdictDir, '002.json'), JSON.stringify({ verdict: 'PASS' }));
|
|
136
|
+
|
|
137
|
+
// Scope 2 is in active, edge active→review has review-gate-check
|
|
138
|
+
const result = service.getReadiness(2)!;
|
|
139
|
+
const toReview = result.transitions.find(t => t.to === 'review');
|
|
140
|
+
if (toReview) {
|
|
141
|
+
const reviewHook = toReview.hooks.find(h => h.id === 'review-gate-check');
|
|
142
|
+
if (reviewHook) {
|
|
143
|
+
expect(reviewHook.status).toBe('pass');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('fails when verdict file is missing', () => {
|
|
149
|
+
const result = service.getReadiness(2)!;
|
|
150
|
+
const toReview = result.transitions.find(t => t.to === 'review');
|
|
151
|
+
if (toReview) {
|
|
152
|
+
const reviewHook = toReview.hooks.find(h => h.id === 'review-gate-check');
|
|
153
|
+
if (reviewHook) {
|
|
154
|
+
expect(reviewHook.status).toBe('fail');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('completion-checklist hook', () => {
|
|
161
|
+
it('fails when scope has unchecked items', () => {
|
|
162
|
+
// Scope 3 has raw_content with "- [ ] Task 1" (unchecked)
|
|
163
|
+
const result = service.getReadiness(3)!;
|
|
164
|
+
for (const transition of result.transitions) {
|
|
165
|
+
const checklist = transition.hooks.find(h => h.id === 'completion-checklist');
|
|
166
|
+
if (checklist) {
|
|
167
|
+
expect(checklist.status).toBe('fail');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('scope-create-gate hook', () => {
|
|
174
|
+
it('passes when scope has title and content', () => {
|
|
175
|
+
const result = service.getReadiness(1)!;
|
|
176
|
+
for (const transition of result.transitions) {
|
|
177
|
+
const gate = transition.hooks.find(h => h.id === 'scope-create-gate');
|
|
178
|
+
if (gate) {
|
|
179
|
+
expect(gate.status).toBe('pass');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('fails when scope has no content', () => {
|
|
185
|
+
scopes.push(makeScope({ id: 6, status: 'backlog', title: '', raw_content: '' }));
|
|
186
|
+
const result = service.getReadiness(6)!;
|
|
187
|
+
for (const transition of result.transitions) {
|
|
188
|
+
const gate = transition.hooks.find(h => h.id === 'scope-create-gate');
|
|
189
|
+
if (gate) {
|
|
190
|
+
expect(gate.status).toBe('fail');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('lifecycle/observer hooks', () => {
|
|
197
|
+
it('scope-transition always passes', () => {
|
|
198
|
+
const result = service.getReadiness(1)!;
|
|
199
|
+
for (const transition of result.transitions) {
|
|
200
|
+
const lifecycle = transition.hooks.find(h => h.id === 'scope-transition');
|
|
201
|
+
if (lifecycle) {
|
|
202
|
+
expect(lifecycle.status).toBe('pass');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('dashboard-sync always passes', () => {
|
|
208
|
+
const result = service.getReadiness(1)!;
|
|
209
|
+
for (const transition of result.transitions) {
|
|
210
|
+
const observer = transition.hooks.find(h => h.id === 'dashboard-sync');
|
|
211
|
+
if (observer) {
|
|
212
|
+
expect(observer.status).toBe('pass');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|