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,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory cache for parsed scopes.
|
|
3
|
+
* Dual-indexed: ID lookups (API, sprint orchestrator) and file-path reverse index (watcher deletions).
|
|
4
|
+
* Replaces the SQLite `scopes` table — filesystem frontmatter is the single source of truth.
|
|
5
|
+
*/
|
|
6
|
+
export class ScopeCache {
|
|
7
|
+
byId = new Map();
|
|
8
|
+
filePathToId = new Map();
|
|
9
|
+
/** Bulk-load all scopes (called at startup from parseAllScopes result) */
|
|
10
|
+
loadAll(scopes) {
|
|
11
|
+
this.byId.clear();
|
|
12
|
+
this.filePathToId.clear();
|
|
13
|
+
for (const scope of scopes) {
|
|
14
|
+
this.byId.set(scope.id, scope);
|
|
15
|
+
this.filePathToId.set(scope.file_path, scope.id);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Insert or update a single scope */
|
|
19
|
+
set(scope) {
|
|
20
|
+
// Clean up old file_path mapping if the scope moved directories
|
|
21
|
+
const existing = this.byId.get(scope.id);
|
|
22
|
+
if (existing && existing.file_path !== scope.file_path) {
|
|
23
|
+
this.filePathToId.delete(existing.file_path);
|
|
24
|
+
}
|
|
25
|
+
this.byId.set(scope.id, scope);
|
|
26
|
+
this.filePathToId.set(scope.file_path, scope.id);
|
|
27
|
+
}
|
|
28
|
+
/** Remove a scope by its file path (used by watcher on file deletion) */
|
|
29
|
+
removeByFilePath(filePath) {
|
|
30
|
+
const id = this.filePathToId.get(filePath);
|
|
31
|
+
if (id !== undefined) {
|
|
32
|
+
this.byId.delete(id);
|
|
33
|
+
this.filePathToId.delete(filePath);
|
|
34
|
+
}
|
|
35
|
+
return id;
|
|
36
|
+
}
|
|
37
|
+
/** Look up scope ID by file path (used before removal to stash status) */
|
|
38
|
+
idByFilePath(filePath) {
|
|
39
|
+
return this.filePathToId.get(filePath);
|
|
40
|
+
}
|
|
41
|
+
/** Check if scope exists by ID */
|
|
42
|
+
has(id) {
|
|
43
|
+
return this.byId.has(id);
|
|
44
|
+
}
|
|
45
|
+
/** Get a scope by ID */
|
|
46
|
+
getById(id) {
|
|
47
|
+
return this.byId.get(id);
|
|
48
|
+
}
|
|
49
|
+
/** Get all scopes sorted by ID */
|
|
50
|
+
getAll() {
|
|
51
|
+
return [...this.byId.values()].sort((a, b) => a.id - b.id);
|
|
52
|
+
}
|
|
53
|
+
/** Get the maximum raw scope number excluding icebox scopes (for next-ID generation).
|
|
54
|
+
* Cache keys use encoded IDs (suffixed scopes like 047a → 1047, 075x → 9075),
|
|
55
|
+
* but next-ID generation needs the raw scope number (047, 075, 087). */
|
|
56
|
+
maxNonIceboxId() {
|
|
57
|
+
let max = 0;
|
|
58
|
+
for (const [id, scope] of this.byId) {
|
|
59
|
+
if (scope.status === 'icebox')
|
|
60
|
+
continue;
|
|
61
|
+
// Decode: encoded IDs ≥1000 have a suffix offset — raw number is id % 1000
|
|
62
|
+
const raw = id >= 1000 ? id % 1000 : id;
|
|
63
|
+
if (raw > max)
|
|
64
|
+
max = raw;
|
|
65
|
+
}
|
|
66
|
+
return max;
|
|
67
|
+
}
|
|
68
|
+
/** Total number of cached scopes */
|
|
69
|
+
get size() {
|
|
70
|
+
return this.byId.size;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
import { normalizeStatus, parseAllScopes, parseScopeFile, setValidStatuses } from '../parsers/scope-parser.js';
|
|
5
|
+
import { createLogger } from '../utils/logger.js';
|
|
6
|
+
const log = createLogger('scope');
|
|
7
|
+
export class ScopeService {
|
|
8
|
+
cache;
|
|
9
|
+
io;
|
|
10
|
+
scopesDir;
|
|
11
|
+
engine;
|
|
12
|
+
onStatusChangeCallbacks = [];
|
|
13
|
+
activeGroupCheck = null;
|
|
14
|
+
suppressedPaths = new Set();
|
|
15
|
+
/** Stash old status when removeByFilePath fires before updateFromFile (chokidar unlink→add) */
|
|
16
|
+
recentlyRemoved = new Map();
|
|
17
|
+
constructor(cache, io, scopesDir, engine) {
|
|
18
|
+
this.cache = cache;
|
|
19
|
+
this.io = io;
|
|
20
|
+
this.scopesDir = scopesDir;
|
|
21
|
+
this.engine = engine;
|
|
22
|
+
}
|
|
23
|
+
/** Register a callback that checks if a scope is in an active group (sprint/batch).
|
|
24
|
+
* Used to guard patch-context status changes. */
|
|
25
|
+
setActiveGroupCheck(fn) {
|
|
26
|
+
this.activeGroupCheck = fn;
|
|
27
|
+
}
|
|
28
|
+
/** Register a callback fired after every successful status update */
|
|
29
|
+
onStatusChange(cb) {
|
|
30
|
+
this.onStatusChangeCallbacks.push(cb);
|
|
31
|
+
}
|
|
32
|
+
/** Load all scopes from the filesystem into the in-memory cache */
|
|
33
|
+
syncFromFilesystem() {
|
|
34
|
+
// Push the engine's valid list IDs to the scope parser so
|
|
35
|
+
// inferStatusFromDir doesn't rely on a hardcoded set.
|
|
36
|
+
setValidStatuses(this.engine.getLists().map(l => l.id));
|
|
37
|
+
const scopes = parseAllScopes(this.scopesDir);
|
|
38
|
+
this.cache.loadAll(scopes);
|
|
39
|
+
return scopes.length;
|
|
40
|
+
}
|
|
41
|
+
/** Check if a path is suppressed from watcher processing (during programmatic moves) */
|
|
42
|
+
isSuppressed(filePath) {
|
|
43
|
+
return this.suppressedPaths.has(filePath);
|
|
44
|
+
}
|
|
45
|
+
/** Re-parse a single scope file and update the cache */
|
|
46
|
+
updateFromFile(filePath) {
|
|
47
|
+
const scope = parseScopeFile(filePath);
|
|
48
|
+
if (!scope)
|
|
49
|
+
return;
|
|
50
|
+
const previous = this.cache.getById(scope.id);
|
|
51
|
+
const previousStatus = previous?.status ?? this.recentlyRemoved.get(scope.id);
|
|
52
|
+
const existing = previous != null;
|
|
53
|
+
this.cache.set(scope);
|
|
54
|
+
this.recentlyRemoved.delete(scope.id);
|
|
55
|
+
const event = existing ? 'scope:updated' : 'scope:created';
|
|
56
|
+
this.io.emit(event, scope);
|
|
57
|
+
// Fire onStatusChange callbacks when status changed via external file move
|
|
58
|
+
// (e.g. scope-transition.sh, manual mv). This ensures batch/sprint
|
|
59
|
+
// orchestrators are notified even when the change bypasses updateStatus().
|
|
60
|
+
// Chokidar fires unlink→add for moves, so the cache entry may already be
|
|
61
|
+
// removed by removeByFilePath — check recentlyRemoved for the old status.
|
|
62
|
+
if (previousStatus != null && previousStatus !== scope.status) {
|
|
63
|
+
for (const cb of this.onStatusChangeCallbacks)
|
|
64
|
+
cb(scope.id, scope.status);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Remove a scope when its file is deleted */
|
|
68
|
+
removeByFilePath(filePath) {
|
|
69
|
+
// Stash status before removal so updateFromFile can detect external moves
|
|
70
|
+
// (chokidar fires unlink before add when a file is moved between directories)
|
|
71
|
+
const scopeId = this.cache.idByFilePath(filePath);
|
|
72
|
+
const previous = scopeId != null ? this.cache.getById(scopeId) : undefined;
|
|
73
|
+
const id = this.cache.removeByFilePath(filePath);
|
|
74
|
+
if (id !== undefined) {
|
|
75
|
+
if (previous)
|
|
76
|
+
this.recentlyRemoved.set(id, previous.status);
|
|
77
|
+
this.io.emit('scope:deleted', id);
|
|
78
|
+
// Clean up stash after a short window (if add never fires, this was a real delete)
|
|
79
|
+
setTimeout(() => this.recentlyRemoved.delete(id), 5000);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Get all scopes (already native arrays/objects — no JSON parsing needed) */
|
|
83
|
+
getAll() {
|
|
84
|
+
return this.cache.getAll();
|
|
85
|
+
}
|
|
86
|
+
/** Get a single scope by ID */
|
|
87
|
+
getById(id) {
|
|
88
|
+
return this.cache.getById(id);
|
|
89
|
+
}
|
|
90
|
+
/** Update a scope's status with transition validation.
|
|
91
|
+
* Writes the new status to the frontmatter file and updates the cache.
|
|
92
|
+
* @param context - caller trust level: 'patch', 'dispatch', 'event', 'bulk-sync', 'rollback' */
|
|
93
|
+
updateStatus(id, status, context = 'patch') {
|
|
94
|
+
if (!this.engine.isValidStatus(status)) {
|
|
95
|
+
return { ok: false, error: `Invalid status: '${status}'`, code: 'INVALID_STATUS' };
|
|
96
|
+
}
|
|
97
|
+
// For non-skip contexts, validate the transition
|
|
98
|
+
if (context !== 'bulk-sync' && context !== 'rollback') {
|
|
99
|
+
const current = this.cache.getById(id);
|
|
100
|
+
if (!current) {
|
|
101
|
+
return { ok: false, error: 'Scope not found', code: 'NOT_FOUND' };
|
|
102
|
+
}
|
|
103
|
+
// Guard: block manual moves for scopes in active groups (sprint/batch)
|
|
104
|
+
if (context === 'patch' && this.activeGroupCheck) {
|
|
105
|
+
const group = this.activeGroupCheck(id);
|
|
106
|
+
if (group) {
|
|
107
|
+
return { ok: false, error: `Scope is in an active ${group.group_type} (ID: ${group.sprint_id})`, code: 'SCOPE_IN_ACTIVE_GROUP' };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const check = this.engine.validateTransition(current.status, status, context);
|
|
111
|
+
if (!check.ok)
|
|
112
|
+
return check;
|
|
113
|
+
}
|
|
114
|
+
// Write to filesystem via updateScopeFrontmatter (which updates cache + emits)
|
|
115
|
+
const current = context === 'bulk-sync' || context === 'rollback'
|
|
116
|
+
? this.cache.getById(id)
|
|
117
|
+
: this.cache.getById(id); // already fetched above for validation, but may be null in bulk-sync
|
|
118
|
+
const fromStatus = current?.status ?? 'unknown';
|
|
119
|
+
const result = this.updateScopeFrontmatter(id, { status }, context);
|
|
120
|
+
if (result.ok) {
|
|
121
|
+
log.info('Status updated', { id, from: fromStatus, to: status, context });
|
|
122
|
+
for (const cb of this.onStatusChangeCallbacks)
|
|
123
|
+
cb(id, status);
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
/** Compute the next sequential scope ID by scanning all non-icebox scopes.
|
|
128
|
+
* Checks both filesystem (all subdirs except icebox) and cache to prevent collisions. */
|
|
129
|
+
getNextScopeId() {
|
|
130
|
+
let maxId = 0;
|
|
131
|
+
// Scan all scope subdirectories except icebox
|
|
132
|
+
if (fs.existsSync(this.scopesDir)) {
|
|
133
|
+
for (const dir of fs.readdirSync(this.scopesDir, { withFileTypes: true })) {
|
|
134
|
+
if (!dir.isDirectory() || dir.name === 'icebox')
|
|
135
|
+
continue;
|
|
136
|
+
const dirPath = path.join(this.scopesDir, dir.name);
|
|
137
|
+
for (const file of fs.readdirSync(dirPath)) {
|
|
138
|
+
const m = file.match(/^(\d+)-/);
|
|
139
|
+
if (m)
|
|
140
|
+
maxId = Math.max(maxId, parseInt(m[1], 10));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Cross-check cache (catches scopes in unexpected locations)
|
|
145
|
+
const cacheMax = this.cache.maxNonIceboxId();
|
|
146
|
+
maxId = Math.max(maxId, cacheMax);
|
|
147
|
+
return maxId + 1;
|
|
148
|
+
}
|
|
149
|
+
// ─── Idea CRUD (filesystem-backed icebox cards) ────────────
|
|
150
|
+
/** Get the next available icebox ID (starts at 501, increments from max found) */
|
|
151
|
+
getNextIceboxId() {
|
|
152
|
+
const iceboxDir = path.join(this.scopesDir, 'icebox');
|
|
153
|
+
if (!fs.existsSync(iceboxDir))
|
|
154
|
+
return 501;
|
|
155
|
+
let maxId = 500;
|
|
156
|
+
for (const file of fs.readdirSync(iceboxDir)) {
|
|
157
|
+
const m = file.match(/^(\d+)-/);
|
|
158
|
+
if (m)
|
|
159
|
+
maxId = Math.max(maxId, parseInt(m[1], 10));
|
|
160
|
+
}
|
|
161
|
+
return maxId + 1;
|
|
162
|
+
}
|
|
163
|
+
/** Find an icebox file by its ID prefix.
|
|
164
|
+
* Matches both padded (091-) and unpadded (91-) filenames
|
|
165
|
+
* since demoted scopes keep their 3-digit-padded names. */
|
|
166
|
+
findIdeaFile(iceboxDir, id) {
|
|
167
|
+
if (!fs.existsSync(iceboxDir))
|
|
168
|
+
return null;
|
|
169
|
+
const match = fs.readdirSync(iceboxDir).find((f) => {
|
|
170
|
+
if (!f.endsWith('.md'))
|
|
171
|
+
return false;
|
|
172
|
+
const m = f.match(/^(\d+)-/);
|
|
173
|
+
return m != null && parseInt(m[1], 10) === id;
|
|
174
|
+
});
|
|
175
|
+
return match ? path.join(iceboxDir, match) : null;
|
|
176
|
+
}
|
|
177
|
+
/** Create an icebox idea as a markdown file. IDs start at 501. */
|
|
178
|
+
createIdeaFile(title, description) {
|
|
179
|
+
const iceboxDir = path.join(this.scopesDir, 'icebox');
|
|
180
|
+
if (!fs.existsSync(iceboxDir))
|
|
181
|
+
fs.mkdirSync(iceboxDir, { recursive: true });
|
|
182
|
+
const nextId = this.getNextIceboxId();
|
|
183
|
+
const slug = title
|
|
184
|
+
.toLowerCase()
|
|
185
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
186
|
+
.replace(/^-|-$/g, '')
|
|
187
|
+
.slice(0, 60);
|
|
188
|
+
const fileName = `${nextId}-${slug}.md`;
|
|
189
|
+
const filePath = path.join(iceboxDir, fileName);
|
|
190
|
+
const now = new Date().toISOString().split('T')[0];
|
|
191
|
+
const content = [
|
|
192
|
+
'---',
|
|
193
|
+
`id: ${nextId}`,
|
|
194
|
+
`title: "${title.replace(/"/g, '\\"')}"`,
|
|
195
|
+
'status: icebox',
|
|
196
|
+
`created: ${now}`,
|
|
197
|
+
`updated: ${now}`,
|
|
198
|
+
'blocked_by: []',
|
|
199
|
+
'blocks: []',
|
|
200
|
+
'tags: []',
|
|
201
|
+
'---',
|
|
202
|
+
'',
|
|
203
|
+
description || '',
|
|
204
|
+
'',
|
|
205
|
+
].join('\n');
|
|
206
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
207
|
+
// Eagerly sync to cache + emit scope:created
|
|
208
|
+
this.updateFromFile(filePath);
|
|
209
|
+
log.info('Idea created', { id: nextId, title });
|
|
210
|
+
return { id: nextId, title };
|
|
211
|
+
}
|
|
212
|
+
/** Update an icebox idea's title and description by rewriting its file */
|
|
213
|
+
updateIdeaFile(id, title, description) {
|
|
214
|
+
const iceboxDir = path.join(this.scopesDir, 'icebox');
|
|
215
|
+
const filePath = this.findIdeaFile(iceboxDir, id);
|
|
216
|
+
if (!filePath)
|
|
217
|
+
return false;
|
|
218
|
+
// Preserve the original created date from existing frontmatter
|
|
219
|
+
const existing = fs.readFileSync(filePath, 'utf-8');
|
|
220
|
+
const createdMatch = existing.match(/^created:\s*(.+)$/m);
|
|
221
|
+
const created = createdMatch?.[1]?.trim() ?? new Date().toISOString().split('T')[0];
|
|
222
|
+
const now = new Date().toISOString().split('T')[0];
|
|
223
|
+
const content = [
|
|
224
|
+
'---',
|
|
225
|
+
`id: ${id}`,
|
|
226
|
+
`title: "${title.replace(/"/g, '\\"')}"`,
|
|
227
|
+
'status: icebox',
|
|
228
|
+
`created: ${created}`,
|
|
229
|
+
`updated: ${now}`,
|
|
230
|
+
'blocked_by: []',
|
|
231
|
+
'blocks: []',
|
|
232
|
+
'tags: []',
|
|
233
|
+
'---',
|
|
234
|
+
'',
|
|
235
|
+
description || '',
|
|
236
|
+
'',
|
|
237
|
+
].join('\n');
|
|
238
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
239
|
+
// Watcher handles cache sync + scope:updated event
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
/** Delete an icebox idea by removing its file */
|
|
243
|
+
deleteIdeaFile(id) {
|
|
244
|
+
const iceboxDir = path.join(this.scopesDir, 'icebox');
|
|
245
|
+
const filePath = this.findIdeaFile(iceboxDir, id);
|
|
246
|
+
if (!filePath)
|
|
247
|
+
return false;
|
|
248
|
+
fs.unlinkSync(filePath);
|
|
249
|
+
// Eagerly remove from cache + emit scope:deleted
|
|
250
|
+
this.removeByFilePath(filePath);
|
|
251
|
+
log.info('Idea deleted', { id });
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
/** Promote an icebox idea to planning — assigns a proper sequential scope ID,
|
|
255
|
+
* moves the file, and syncs cache. Returns the new scope ID. */
|
|
256
|
+
promoteIdea(id) {
|
|
257
|
+
const iceboxDir = path.join(this.scopesDir, 'icebox');
|
|
258
|
+
const oldPath = this.findIdeaFile(iceboxDir, id);
|
|
259
|
+
if (!oldPath)
|
|
260
|
+
return null;
|
|
261
|
+
// Read existing file for metadata
|
|
262
|
+
const content = fs.readFileSync(oldPath, 'utf-8');
|
|
263
|
+
const titleMatch = content.match(/^title:\s*"?([^"\n]+)"?\s*$/m);
|
|
264
|
+
const createdMatch = content.match(/^created:\s*(.+)$/m);
|
|
265
|
+
const title = titleMatch?.[1]?.trim() ?? 'Untitled';
|
|
266
|
+
const created = createdMatch?.[1]?.trim() ?? new Date().toISOString().split('T')[0];
|
|
267
|
+
// Extract body after frontmatter
|
|
268
|
+
const fmEnd = content.indexOf('---', content.indexOf('---') + 3);
|
|
269
|
+
const description = fmEnd !== -1 ? content.slice(fmEnd + 3).trim() : '';
|
|
270
|
+
// Assign the next sequential scope ID (excludes icebox items)
|
|
271
|
+
const newId = this.getNextScopeId();
|
|
272
|
+
const paddedId = String(newId).padStart(3, '0');
|
|
273
|
+
// Build slug and new path
|
|
274
|
+
const slug = title
|
|
275
|
+
.toLowerCase()
|
|
276
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
277
|
+
.replace(/^-|-$/g, '')
|
|
278
|
+
.slice(0, 60);
|
|
279
|
+
const planningDir = path.join(this.scopesDir, 'planning');
|
|
280
|
+
if (!fs.existsSync(planningDir))
|
|
281
|
+
fs.mkdirSync(planningDir, { recursive: true });
|
|
282
|
+
const newFileName = `${paddedId}-${slug}.md`;
|
|
283
|
+
const newPath = path.join(planningDir, newFileName);
|
|
284
|
+
const now = new Date().toISOString().split('T')[0];
|
|
285
|
+
// Write new file with planning status and new sequential ID
|
|
286
|
+
const newContent = [
|
|
287
|
+
'---',
|
|
288
|
+
`id: ${paddedId}`,
|
|
289
|
+
`title: "${title.replace(/"/g, '\\"')}"`,
|
|
290
|
+
'status: planning',
|
|
291
|
+
`created: ${created}`,
|
|
292
|
+
`updated: ${now}`,
|
|
293
|
+
'blocked_by: []',
|
|
294
|
+
'blocks: []',
|
|
295
|
+
'tags: []',
|
|
296
|
+
'---',
|
|
297
|
+
'',
|
|
298
|
+
description || '',
|
|
299
|
+
'',
|
|
300
|
+
].join('\n');
|
|
301
|
+
fs.writeFileSync(newPath, newContent, 'utf-8');
|
|
302
|
+
// Sync cache before deleting old file (avoids window where scope is missing)
|
|
303
|
+
this.updateFromFile(newPath);
|
|
304
|
+
fs.unlinkSync(oldPath);
|
|
305
|
+
this.removeByFilePath(oldPath);
|
|
306
|
+
const relPath = path.relative(path.resolve(this.scopesDir, '..'), newPath);
|
|
307
|
+
log.info('Idea promoted', { oldId: id, newId, title });
|
|
308
|
+
return { id: newId, filePath: relPath, title, description };
|
|
309
|
+
}
|
|
310
|
+
/** Find a scope file by its numeric ID prefix across all status directories */
|
|
311
|
+
findScopeFile(id) {
|
|
312
|
+
if (!fs.existsSync(this.scopesDir))
|
|
313
|
+
return null;
|
|
314
|
+
const paddedId = String(id).padStart(3, '0');
|
|
315
|
+
const prefixes = [`${id}-`, `${paddedId}-`];
|
|
316
|
+
for (const dir of fs.readdirSync(this.scopesDir, { withFileTypes: true })) {
|
|
317
|
+
if (!dir.isDirectory())
|
|
318
|
+
continue;
|
|
319
|
+
const dirPath = path.join(this.scopesDir, dir.name);
|
|
320
|
+
for (const file of fs.readdirSync(dirPath)) {
|
|
321
|
+
if (file.endsWith('.md') && prefixes.some((p) => file.startsWith(p))) {
|
|
322
|
+
return path.join(dirPath, file);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
/** Update a scope's frontmatter fields and write back to the .md file.
|
|
329
|
+
* If status changes, validates the transition and moves the file to the new status directory.
|
|
330
|
+
* @param context - transition context for validation (default 'patch') */
|
|
331
|
+
updateScopeFrontmatter(id, fields, context = 'patch') {
|
|
332
|
+
const filePath = this.findScopeFile(id);
|
|
333
|
+
if (!filePath) {
|
|
334
|
+
return { ok: false, error: 'Scope file not found', code: 'NOT_FOUND' };
|
|
335
|
+
}
|
|
336
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
337
|
+
const parsed = matter(raw);
|
|
338
|
+
const today = new Date().toISOString().split('T')[0];
|
|
339
|
+
// Validate status transition before any writes
|
|
340
|
+
const newStatus = fields.status;
|
|
341
|
+
const rawOldStatus = String(parsed.data.status ?? 'planning');
|
|
342
|
+
const oldStatus = normalizeStatus(rawOldStatus);
|
|
343
|
+
let needsMove = false;
|
|
344
|
+
if (newStatus && newStatus !== oldStatus) {
|
|
345
|
+
if (!this.engine.isValidStatus(newStatus)) {
|
|
346
|
+
return { ok: false, error: `Invalid status: '${newStatus}'`, code: 'INVALID_STATUS' };
|
|
347
|
+
}
|
|
348
|
+
const check = this.engine.validateTransition(oldStatus, newStatus, context);
|
|
349
|
+
if (!check.ok)
|
|
350
|
+
return check;
|
|
351
|
+
needsMove = true;
|
|
352
|
+
// Auto-unlock spec when reverting backlog → planning
|
|
353
|
+
if (newStatus === 'planning' && oldStatus === 'backlog')
|
|
354
|
+
fields.spec_locked = false;
|
|
355
|
+
}
|
|
356
|
+
// Merge editable fields into frontmatter
|
|
357
|
+
const editableKeys = ['title', 'status', 'priority', 'effort_estimate', 'category', 'tags', 'blocked_by', 'blocks', 'spec_locked'];
|
|
358
|
+
for (const key of editableKeys) {
|
|
359
|
+
if (key in fields) {
|
|
360
|
+
const val = fields[key];
|
|
361
|
+
// Treat empty strings / null as removal (delete the key)
|
|
362
|
+
if (val === null || val === '' || val === 'none') {
|
|
363
|
+
delete parsed.data[key];
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
parsed.data[key] = val;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
parsed.data.updated = today;
|
|
371
|
+
// Normalize Date objects to YYYY-MM-DD strings to prevent matter.stringify
|
|
372
|
+
// from converting them to full ISO timestamps (gray-matter auto-parses bare dates)
|
|
373
|
+
for (const key of Object.keys(parsed.data)) {
|
|
374
|
+
const val = parsed.data[key];
|
|
375
|
+
if (val instanceof Date) {
|
|
376
|
+
parsed.data[key] = val.toISOString().split('T')[0];
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (!needsMove) {
|
|
380
|
+
// Simple in-place rewrite
|
|
381
|
+
fs.writeFileSync(filePath, matter.stringify(parsed.content, parsed.data), 'utf-8');
|
|
382
|
+
// Chokidar will pick this up, but eagerly sync for instant feedback
|
|
383
|
+
this.updateFromFile(filePath);
|
|
384
|
+
log.info('Frontmatter updated', { id, fields: Object.keys(fields) });
|
|
385
|
+
return { ok: true };
|
|
386
|
+
}
|
|
387
|
+
// Status change → move file to new directory
|
|
388
|
+
const targetDir = path.join(this.scopesDir, newStatus);
|
|
389
|
+
if (!fs.existsSync(targetDir))
|
|
390
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
391
|
+
const fileName = path.basename(filePath);
|
|
392
|
+
const newPath = path.join(targetDir, fileName);
|
|
393
|
+
const newContent = matter.stringify(parsed.content, parsed.data);
|
|
394
|
+
// Suppress watcher events during programmatic move to prevent race conditions
|
|
395
|
+
this.suppressedPaths.add(filePath);
|
|
396
|
+
this.suppressedPaths.add(newPath);
|
|
397
|
+
// Update content in-place, then atomic rename (no window where file is missing)
|
|
398
|
+
fs.writeFileSync(filePath, newContent, 'utf-8');
|
|
399
|
+
fs.renameSync(filePath, newPath);
|
|
400
|
+
this.updateFromFile(newPath);
|
|
401
|
+
this.removeByFilePath(filePath);
|
|
402
|
+
// Clear suppression after watcher events have drained
|
|
403
|
+
setTimeout(() => {
|
|
404
|
+
this.suppressedPaths.delete(filePath);
|
|
405
|
+
this.suppressedPaths.delete(newPath);
|
|
406
|
+
}, 500);
|
|
407
|
+
return { ok: true, moved: true };
|
|
408
|
+
}
|
|
409
|
+
/** Approve a ghost idea — removes ghost:true from frontmatter and refreshes cache */
|
|
410
|
+
approveGhostIdea(id) {
|
|
411
|
+
const iceboxDir = path.join(this.scopesDir, 'icebox');
|
|
412
|
+
const filePath = this.findIdeaFile(iceboxDir, id);
|
|
413
|
+
if (!filePath)
|
|
414
|
+
return false;
|
|
415
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
416
|
+
// Remove ghost: true line from frontmatter
|
|
417
|
+
const updated = content.replace(/^ghost:\s*true\n/m, '');
|
|
418
|
+
fs.writeFileSync(filePath, updated, 'utf-8');
|
|
419
|
+
// Re-parse file to refresh cache with is_ghost=false
|
|
420
|
+
this.updateFromFile(filePath);
|
|
421
|
+
log.info('Ghost approved', { id });
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
}
|