@amsterdamdatalabs/enact-factory 0.1.1
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 +674 -0
- package/README.md +566 -0
- package/dist/adapters/agenticLoop.d.ts +90 -0
- package/dist/adapters/agenticLoop.d.ts.map +1 -0
- package/dist/adapters/agenticLoop.js +219 -0
- package/dist/adapters/agenticLoop.js.map +1 -0
- package/dist/adapters/base.d.ts +16 -0
- package/dist/adapters/base.d.ts.map +1 -0
- package/dist/adapters/base.js +135 -0
- package/dist/adapters/base.js.map +1 -0
- package/dist/adapters/claude.d.ts +13 -0
- package/dist/adapters/claude.d.ts.map +1 -0
- package/dist/adapters/claude.js +318 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/codex.d.ts +14 -0
- package/dist/adapters/codex.d.ts.map +1 -0
- package/dist/adapters/codex.js +366 -0
- package/dist/adapters/codex.js.map +1 -0
- package/dist/adapters/cryptoQuantAdapter.d.ts +85 -0
- package/dist/adapters/cryptoQuantAdapter.d.ts.map +1 -0
- package/dist/adapters/cryptoQuantAdapter.js +238 -0
- package/dist/adapters/cryptoQuantAdapter.js.map +1 -0
- package/dist/adapters/cursor.d.ts +13 -0
- package/dist/adapters/cursor.d.ts.map +1 -0
- package/dist/adapters/cursor.js +300 -0
- package/dist/adapters/cursor.js.map +1 -0
- package/dist/adapters/envPath.d.ts +20 -0
- package/dist/adapters/envPath.d.ts.map +1 -0
- package/dist/adapters/envPath.js +49 -0
- package/dist/adapters/envPath.js.map +1 -0
- package/dist/adapters/hermes.d.ts +13 -0
- package/dist/adapters/hermes.d.ts.map +1 -0
- package/dist/adapters/hermes.js +283 -0
- package/dist/adapters/hermes.js.map +1 -0
- package/dist/adapters/index.d.ts +18 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +56 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/opencode.d.ts +13 -0
- package/dist/adapters/opencode.d.ts.map +1 -0
- package/dist/adapters/opencode.js +282 -0
- package/dist/adapters/opencode.js.map +1 -0
- package/dist/adapters/processRegistry.d.ts +38 -0
- package/dist/adapters/processRegistry.d.ts.map +1 -0
- package/dist/adapters/processRegistry.js +147 -0
- package/dist/adapters/processRegistry.js.map +1 -0
- package/dist/adapters/responses.d.ts +16 -0
- package/dist/adapters/responses.d.ts.map +1 -0
- package/dist/adapters/responses.js +244 -0
- package/dist/adapters/responses.js.map +1 -0
- package/dist/adapters/streamBuffer.d.ts +59 -0
- package/dist/adapters/streamBuffer.d.ts.map +1 -0
- package/dist/adapters/streamBuffer.js +123 -0
- package/dist/adapters/streamBuffer.js.map +1 -0
- package/dist/adapters/tools.d.ts +30 -0
- package/dist/adapters/tools.d.ts.map +1 -0
- package/dist/adapters/tools.js +219 -0
- package/dist/adapters/tools.js.map +1 -0
- package/dist/adapters/types.d.ts +82 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +6 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/agents/agentBus.d.ts +160 -0
- package/dist/agents/agentBus.d.ts.map +1 -0
- package/dist/agents/agentBus.js +350 -0
- package/dist/agents/agentBus.js.map +1 -0
- package/dist/agents/agentPair.d.ts +215 -0
- package/dist/agents/agentPair.d.ts.map +1 -0
- package/dist/agents/agentPair.js +456 -0
- package/dist/agents/agentPair.js.map +1 -0
- package/dist/agents/auditor.d.ts +27 -0
- package/dist/agents/auditor.d.ts.map +1 -0
- package/dist/agents/auditor.js +238 -0
- package/dist/agents/auditor.js.map +1 -0
- package/dist/agents/cliStreamParser.d.ts +18 -0
- package/dist/agents/cliStreamParser.d.ts.map +1 -0
- package/dist/agents/cliStreamParser.js +156 -0
- package/dist/agents/cliStreamParser.js.map +1 -0
- package/dist/agents/documenter.d.ts +31 -0
- package/dist/agents/documenter.d.ts.map +1 -0
- package/dist/agents/documenter.js +286 -0
- package/dist/agents/documenter.js.map +1 -0
- package/dist/agents/draftAnalyzer.d.ts +50 -0
- package/dist/agents/draftAnalyzer.d.ts.map +1 -0
- package/dist/agents/draftAnalyzer.js +289 -0
- package/dist/agents/draftAnalyzer.js.map +1 -0
- package/dist/agents/evaluator.d.ts +61 -0
- package/dist/agents/evaluator.d.ts.map +1 -0
- package/dist/agents/evaluator.js +338 -0
- package/dist/agents/evaluator.js.map +1 -0
- package/dist/agents/executor.d.ts +33 -0
- package/dist/agents/executor.d.ts.map +1 -0
- package/dist/agents/executor.js +130 -0
- package/dist/agents/executor.js.map +1 -0
- package/dist/agents/index.d.ts +10 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +10 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/pairMetrics.d.ts +63 -0
- package/dist/agents/pairMetrics.d.ts.map +1 -0
- package/dist/agents/pairMetrics.js +232 -0
- package/dist/agents/pairMetrics.js.map +1 -0
- package/dist/agents/pairPipeline.d.ts +184 -0
- package/dist/agents/pairPipeline.d.ts.map +1 -0
- package/dist/agents/pairPipeline.js +934 -0
- package/dist/agents/pairPipeline.js.map +1 -0
- package/dist/agents/pairWebhook.d.ts +59 -0
- package/dist/agents/pairWebhook.d.ts.map +1 -0
- package/dist/agents/pairWebhook.js +242 -0
- package/dist/agents/pairWebhook.js.map +1 -0
- package/dist/agents/pipelineFormat.d.ts +8 -0
- package/dist/agents/pipelineFormat.d.ts.map +1 -0
- package/dist/agents/pipelineFormat.js +65 -0
- package/dist/agents/pipelineFormat.js.map +1 -0
- package/dist/agents/pipelineGuards.d.ts +23 -0
- package/dist/agents/pipelineGuards.d.ts.map +1 -0
- package/dist/agents/pipelineGuards.js +257 -0
- package/dist/agents/pipelineGuards.js.map +1 -0
- package/dist/agents/reviewer.d.ts +37 -0
- package/dist/agents/reviewer.d.ts.map +1 -0
- package/dist/agents/reviewer.js +214 -0
- package/dist/agents/reviewer.js.map +1 -0
- package/dist/agents/skillDocumenter.d.ts +23 -0
- package/dist/agents/skillDocumenter.d.ts.map +1 -0
- package/dist/agents/skillDocumenter.js +219 -0
- package/dist/agents/skillDocumenter.js.map +1 -0
- package/dist/agents/tester.d.ts +37 -0
- package/dist/agents/tester.d.ts.map +1 -0
- package/dist/agents/tester.js +309 -0
- package/dist/agents/tester.js.map +1 -0
- package/dist/automation/autonomousRunner.d.ts +145 -0
- package/dist/automation/autonomousRunner.d.ts.map +1 -0
- package/dist/automation/autonomousRunner.js +1272 -0
- package/dist/automation/autonomousRunner.js.map +1 -0
- package/dist/automation/dailyReporter.d.ts +26 -0
- package/dist/automation/dailyReporter.d.ts.map +1 -0
- package/dist/automation/dailyReporter.js +130 -0
- package/dist/automation/dailyReporter.js.map +1 -0
- package/dist/automation/index.d.ts +5 -0
- package/dist/automation/index.d.ts.map +1 -0
- package/dist/automation/index.js +5 -0
- package/dist/automation/index.js.map +1 -0
- package/dist/automation/longRunningMonitor.d.ts +26 -0
- package/dist/automation/longRunningMonitor.d.ts.map +1 -0
- package/dist/automation/longRunningMonitor.js +356 -0
- package/dist/automation/longRunningMonitor.js.map +1 -0
- package/dist/automation/prOwnership.d.ts +18 -0
- package/dist/automation/prOwnership.d.ts.map +1 -0
- package/dist/automation/prOwnership.js +61 -0
- package/dist/automation/prOwnership.js.map +1 -0
- package/dist/automation/runnerExecution.d.ts +57 -0
- package/dist/automation/runnerExecution.d.ts.map +1 -0
- package/dist/automation/runnerExecution.js +701 -0
- package/dist/automation/runnerExecution.js.map +1 -0
- package/dist/automation/runnerState.d.ts +170 -0
- package/dist/automation/runnerState.d.ts.map +1 -0
- package/dist/automation/runnerState.js +496 -0
- package/dist/automation/runnerState.js.map +1 -0
- package/dist/automation/runnerTypes.d.ts +57 -0
- package/dist/automation/runnerTypes.d.ts.map +1 -0
- package/dist/automation/runnerTypes.js +5 -0
- package/dist/automation/runnerTypes.js.map +1 -0
- package/dist/automation/scheduler.d.ts +75 -0
- package/dist/automation/scheduler.d.ts.map +1 -0
- package/dist/automation/scheduler.js +402 -0
- package/dist/automation/scheduler.js.map +1 -0
- package/dist/azdo/azdo.d.ts +70 -0
- package/dist/azdo/azdo.d.ts.map +1 -0
- package/dist/azdo/azdo.js +328 -0
- package/dist/azdo/azdo.js.map +1 -0
- package/dist/azdo/index.d.ts +3 -0
- package/dist/azdo/index.d.ts.map +1 -0
- package/dist/azdo/index.js +3 -0
- package/dist/azdo/index.js.map +1 -0
- package/dist/azdo/projectUpdater.d.ts +13 -0
- package/dist/azdo/projectUpdater.d.ts.map +1 -0
- package/dist/azdo/projectUpdater.js +155 -0
- package/dist/azdo/projectUpdater.js.map +1 -0
- package/dist/azureDevOps/client.d.ts +75 -0
- package/dist/azureDevOps/client.d.ts.map +1 -0
- package/dist/azureDevOps/client.js +150 -0
- package/dist/azureDevOps/client.js.map +1 -0
- package/dist/azureDevOps/hierarchy.d.ts +119 -0
- package/dist/azureDevOps/hierarchy.d.ts.map +1 -0
- package/dist/azureDevOps/hierarchy.js +470 -0
- package/dist/azureDevOps/hierarchy.js.map +1 -0
- package/dist/azureDevOps/mapper.d.ts +101 -0
- package/dist/azureDevOps/mapper.d.ts.map +1 -0
- package/dist/azureDevOps/mapper.js +438 -0
- package/dist/azureDevOps/mapper.js.map +1 -0
- package/dist/azureDevOps/stateMapping.d.ts +15 -0
- package/dist/azureDevOps/stateMapping.d.ts.map +1 -0
- package/dist/azureDevOps/stateMapping.js +141 -0
- package/dist/azureDevOps/stateMapping.js.map +1 -0
- package/dist/cli/authHandler.d.ts +13 -0
- package/dist/cli/authHandler.d.ts.map +1 -0
- package/dist/cli/authHandler.js +70 -0
- package/dist/cli/authHandler.js.map +1 -0
- package/dist/cli/checkHandler.d.ts +27 -0
- package/dist/cli/checkHandler.d.ts.map +1 -0
- package/dist/cli/checkHandler.js +560 -0
- package/dist/cli/checkHandler.js.map +1 -0
- package/dist/cli/daemon.d.ts +30 -0
- package/dist/cli/daemon.d.ts.map +1 -0
- package/dist/cli/daemon.js +141 -0
- package/dist/cli/daemon.js.map +1 -0
- package/dist/cli/factoryCommands.d.ts +3 -0
- package/dist/cli/factoryCommands.d.ts.map +1 -0
- package/dist/cli/factoryCommands.js +165 -0
- package/dist/cli/factoryCommands.js.map +1 -0
- package/dist/cli/promptHandler.d.ts +13 -0
- package/dist/cli/promptHandler.d.ts.map +1 -0
- package/dist/cli/promptHandler.js +193 -0
- package/dist/cli/promptHandler.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +320 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/agentLifecycle.d.ts +322 -0
- package/dist/core/agentLifecycle.d.ts.map +1 -0
- package/dist/core/agentLifecycle.js +230 -0
- package/dist/core/agentLifecycle.js.map +1 -0
- package/dist/core/areaMapping.d.ts +9 -0
- package/dist/core/areaMapping.d.ts.map +1 -0
- package/dist/core/areaMapping.js +37 -0
- package/dist/core/areaMapping.js.map +1 -0
- package/dist/core/config.d.ts +469 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +780 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/dashboardContract.d.ts +204 -0
- package/dist/core/dashboardContract.d.ts.map +1 -0
- package/dist/core/dashboardContract.js +205 -0
- package/dist/core/dashboardContract.js.map +1 -0
- package/dist/core/devopsModel.d.ts +138 -0
- package/dist/core/devopsModel.d.ts.map +1 -0
- package/dist/core/devopsModel.js +137 -0
- package/dist/core/devopsModel.js.map +1 -0
- package/dist/core/envFile.d.ts +11 -0
- package/dist/core/envFile.d.ts.map +1 -0
- package/dist/core/envFile.js +104 -0
- package/dist/core/envFile.js.map +1 -0
- package/dist/core/eventHub.d.ts +220 -0
- package/dist/core/eventHub.d.ts.map +1 -0
- package/dist/core/eventHub.js +136 -0
- package/dist/core/eventHub.js.map +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +7 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/laneExecutionState.d.ts +29 -0
- package/dist/core/laneExecutionState.d.ts.map +1 -0
- package/dist/core/laneExecutionState.js +18 -0
- package/dist/core/laneExecutionState.js.map +1 -0
- package/dist/core/laneStatus.d.ts +49 -0
- package/dist/core/laneStatus.d.ts.map +1 -0
- package/dist/core/laneStatus.js +153 -0
- package/dist/core/laneStatus.js.map +1 -0
- package/dist/core/prSidecar.d.ts +96 -0
- package/dist/core/prSidecar.d.ts.map +1 -0
- package/dist/core/prSidecar.js +33 -0
- package/dist/core/prSidecar.js.map +1 -0
- package/dist/core/runtimeConfig.d.ts +6 -0
- package/dist/core/runtimeConfig.d.ts.map +1 -0
- package/dist/core/runtimeConfig.js +24 -0
- package/dist/core/runtimeConfig.js.map +1 -0
- package/dist/core/scmProvider.d.ts +19 -0
- package/dist/core/scmProvider.d.ts.map +1 -0
- package/dist/core/scmProvider.js +38 -0
- package/dist/core/scmProvider.js.map +1 -0
- package/dist/core/service.d.ts +10 -0
- package/dist/core/service.d.ts.map +1 -0
- package/dist/core/service.js +297 -0
- package/dist/core/service.js.map +1 -0
- package/dist/core/traceCollector.d.ts +105 -0
- package/dist/core/traceCollector.d.ts.map +1 -0
- package/dist/core/traceCollector.js +141 -0
- package/dist/core/traceCollector.js.map +1 -0
- package/dist/core/types.d.ts +432 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/workItemMapper.d.ts +39 -0
- package/dist/core/workItemMapper.d.ts.map +1 -0
- package/dist/core/workItemMapper.js +427 -0
- package/dist/core/workItemMapper.js.map +1 -0
- package/dist/core/workItemModel.d.ts +120 -0
- package/dist/core/workItemModel.d.ts.map +1 -0
- package/dist/core/workItemModel.js +104 -0
- package/dist/core/workItemModel.js.map +1 -0
- package/dist/core/workItemPayload.d.ts +195 -0
- package/dist/core/workItemPayload.d.ts.map +1 -0
- package/dist/core/workItemPayload.js +24 -0
- package/dist/core/workItemPayload.js.map +1 -0
- package/dist/core/workspaceConfig.d.ts +57 -0
- package/dist/core/workspaceConfig.d.ts.map +1 -0
- package/dist/core/workspaceConfig.js +184 -0
- package/dist/core/workspaceConfig.js.map +1 -0
- package/dist/doctor.d.ts +18 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +34 -0
- package/dist/doctor.js.map +1 -0
- package/dist/factory/activeSkill.d.ts +11 -0
- package/dist/factory/activeSkill.d.ts.map +1 -0
- package/dist/factory/activeSkill.js +44 -0
- package/dist/factory/activeSkill.js.map +1 -0
- package/dist/factory/assignment.d.ts +54 -0
- package/dist/factory/assignment.d.ts.map +1 -0
- package/dist/factory/assignment.js +94 -0
- package/dist/factory/assignment.js.map +1 -0
- package/dist/factory/auditLog.d.ts +10 -0
- package/dist/factory/auditLog.d.ts.map +1 -0
- package/dist/factory/auditLog.js +38 -0
- package/dist/factory/auditLog.js.map +1 -0
- package/dist/factory/closureRequirements.d.ts +12 -0
- package/dist/factory/closureRequirements.d.ts.map +1 -0
- package/dist/factory/closureRequirements.js +30 -0
- package/dist/factory/closureRequirements.js.map +1 -0
- package/dist/factory/delegationPrompt.d.ts +3 -0
- package/dist/factory/delegationPrompt.d.ts.map +1 -0
- package/dist/factory/delegationPrompt.js +16 -0
- package/dist/factory/delegationPrompt.js.map +1 -0
- package/dist/factory/http.d.ts +3 -0
- package/dist/factory/http.d.ts.map +1 -0
- package/dist/factory/http.js +555 -0
- package/dist/factory/http.js.map +1 -0
- package/dist/factory/lifecyclePushMap.d.ts +4 -0
- package/dist/factory/lifecyclePushMap.d.ts.map +1 -0
- package/dist/factory/lifecyclePushMap.js +7 -0
- package/dist/factory/lifecyclePushMap.js.map +1 -0
- package/dist/factory/missions.d.ts +125 -0
- package/dist/factory/missions.d.ts.map +1 -0
- package/dist/factory/missions.js +304 -0
- package/dist/factory/missions.js.map +1 -0
- package/dist/factory/mode.d.ts +9 -0
- package/dist/factory/mode.d.ts.map +1 -0
- package/dist/factory/mode.js +30 -0
- package/dist/factory/mode.js.map +1 -0
- package/dist/factory/operatorActiveSkill.d.ts +15 -0
- package/dist/factory/operatorActiveSkill.d.ts.map +1 -0
- package/dist/factory/operatorActiveSkill.js +95 -0
- package/dist/factory/operatorActiveSkill.js.map +1 -0
- package/dist/factory/paseoDispatcher.d.ts +52 -0
- package/dist/factory/paseoDispatcher.d.ts.map +1 -0
- package/dist/factory/paseoDispatcher.js +122 -0
- package/dist/factory/paseoDispatcher.js.map +1 -0
- package/dist/factory/paseoLifecycle.d.ts +32 -0
- package/dist/factory/paseoLifecycle.d.ts.map +1 -0
- package/dist/factory/paseoLifecycle.js +260 -0
- package/dist/factory/paseoLifecycle.js.map +1 -0
- package/dist/factory/paths.d.ts +31 -0
- package/dist/factory/paths.d.ts.map +1 -0
- package/dist/factory/paths.js +139 -0
- package/dist/factory/paths.js.map +1 -0
- package/dist/factory/progressWatchdog.d.ts +58 -0
- package/dist/factory/progressWatchdog.d.ts.map +1 -0
- package/dist/factory/progressWatchdog.js +160 -0
- package/dist/factory/progressWatchdog.js.map +1 -0
- package/dist/factory/roster.d.ts +59 -0
- package/dist/factory/roster.d.ts.map +1 -0
- package/dist/factory/roster.js +116 -0
- package/dist/factory/roster.js.map +1 -0
- package/dist/factory/runtime.d.ts +44 -0
- package/dist/factory/runtime.d.ts.map +1 -0
- package/dist/factory/runtime.js +238 -0
- package/dist/factory/runtime.js.map +1 -0
- package/dist/factory/sync.d.ts +29 -0
- package/dist/factory/sync.d.ts.map +1 -0
- package/dist/factory/sync.js +77 -0
- package/dist/factory/sync.js.map +1 -0
- package/dist/factory/workitemQueues.d.ts +37 -0
- package/dist/factory/workitemQueues.d.ts.map +1 -0
- package/dist/factory/workitemQueues.js +99 -0
- package/dist/factory/workitemQueues.js.map +1 -0
- package/dist/factory/workitemTriage.d.ts +9 -0
- package/dist/factory/workitemTriage.d.ts.map +1 -0
- package/dist/factory/workitemTriage.js +81 -0
- package/dist/factory/workitemTriage.js.map +1 -0
- package/dist/hooks.d.ts +18 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +96 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +90 -0
- package/dist/index.js.map +1 -0
- package/dist/install/agentCatalog.d.ts +7 -0
- package/dist/install/agentCatalog.d.ts.map +1 -0
- package/dist/install/agentCatalog.js +28 -0
- package/dist/install/agentCatalog.js.map +1 -0
- package/dist/install/bundlePaths.d.ts +10 -0
- package/dist/install/bundlePaths.d.ts.map +1 -0
- package/dist/install/bundlePaths.js +30 -0
- package/dist/install/bundlePaths.js.map +1 -0
- package/dist/install/codex.d.ts +43 -0
- package/dist/install/codex.d.ts.map +1 -0
- package/dist/install/codex.js +207 -0
- package/dist/install/codex.js.map +1 -0
- package/dist/install/enactHome.d.ts +37 -0
- package/dist/install/enactHome.d.ts.map +1 -0
- package/dist/install/enactHome.js +152 -0
- package/dist/install/enactHome.js.map +1 -0
- package/dist/install/plugins.d.ts +115 -0
- package/dist/install/plugins.d.ts.map +1 -0
- package/dist/install/plugins.js +259 -0
- package/dist/install/plugins.js.map +1 -0
- package/dist/install/setup.d.ts +33 -0
- package/dist/install/setup.d.ts.map +1 -0
- package/dist/install/setup.js +167 -0
- package/dist/install/setup.js.map +1 -0
- package/dist/locale/en.d.ts +3 -0
- package/dist/locale/en.d.ts.map +1 -0
- package/dist/locale/en.js +435 -0
- package/dist/locale/en.js.map +1 -0
- package/dist/locale/index.d.ts +28 -0
- package/dist/locale/index.d.ts.map +1 -0
- package/dist/locale/index.js +84 -0
- package/dist/locale/index.js.map +1 -0
- package/dist/locale/prompts/en.d.ts +3 -0
- package/dist/locale/prompts/en.d.ts.map +1 -0
- package/dist/locale/prompts/en.js +254 -0
- package/dist/locale/prompts/en.js.map +1 -0
- package/dist/locale/types.d.ts +433 -0
- package/dist/locale/types.d.ts.map +1 -0
- package/dist/locale/types.js +5 -0
- package/dist/locale/types.js.map +1 -0
- package/dist/mcp/server.d.ts +489 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +597 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/orchestration/decisionEngine.d.ts +175 -0
- package/dist/orchestration/decisionEngine.d.ts.map +1 -0
- package/dist/orchestration/decisionEngine.js +471 -0
- package/dist/orchestration/decisionEngine.js.map +1 -0
- package/dist/orchestration/index.d.ts +5 -0
- package/dist/orchestration/index.d.ts.map +1 -0
- package/dist/orchestration/index.js +5 -0
- package/dist/orchestration/index.js.map +1 -0
- package/dist/orchestration/workItemParser.d.ts +67 -0
- package/dist/orchestration/workItemParser.d.ts.map +1 -0
- package/dist/orchestration/workItemParser.js +560 -0
- package/dist/orchestration/workItemParser.js.map +1 -0
- package/dist/orchestration/workItemScheduler.d.ts +141 -0
- package/dist/orchestration/workItemScheduler.d.ts.map +1 -0
- package/dist/orchestration/workItemScheduler.js +317 -0
- package/dist/orchestration/workItemScheduler.js.map +1 -0
- package/dist/orchestration/workflow.d.ts +145 -0
- package/dist/orchestration/workflow.d.ts.map +1 -0
- package/dist/orchestration/workflow.js +301 -0
- package/dist/orchestration/workflow.js.map +1 -0
- package/dist/providers/codexSessions.d.ts +93 -0
- package/dist/providers/codexSessions.d.ts.map +1 -0
- package/dist/providers/codexSessions.js +366 -0
- package/dist/providers/codexSessions.js.map +1 -0
- package/dist/registry/bsDetector.d.ts +24 -0
- package/dist/registry/bsDetector.d.ts.map +1 -0
- package/dist/registry/bsDetector.js +276 -0
- package/dist/registry/bsDetector.js.map +1 -0
- package/dist/registry/entityScanner.d.ts +36 -0
- package/dist/registry/entityScanner.d.ts.map +1 -0
- package/dist/registry/entityScanner.js +693 -0
- package/dist/registry/entityScanner.js.map +1 -0
- package/dist/registry/index.d.ts +9 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +13 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/registry/schema.d.ts +307 -0
- package/dist/registry/schema.d.ts.map +1 -0
- package/dist/registry/schema.js +139 -0
- package/dist/registry/schema.js.map +1 -0
- package/dist/registry/sqliteStore.d.ts +101 -0
- package/dist/registry/sqliteStore.d.ts.map +1 -0
- package/dist/registry/sqliteStore.js +688 -0
- package/dist/registry/sqliteStore.js.map +1 -0
- package/dist/registry/workItemBridge.d.ts +8 -0
- package/dist/registry/workItemBridge.d.ts.map +1 -0
- package/dist/registry/workItemBridge.js +30 -0
- package/dist/registry/workItemBridge.js.map +1 -0
- package/dist/runners/cliRunner.d.ts +11 -0
- package/dist/runners/cliRunner.d.ts.map +1 -0
- package/dist/runners/cliRunner.js +193 -0
- package/dist/runners/cliRunner.js.map +1 -0
- package/dist/support/apiCache.d.ts +85 -0
- package/dist/support/apiCache.d.ts.map +1 -0
- package/dist/support/apiCache.js +163 -0
- package/dist/support/apiCache.js.map +1 -0
- package/dist/support/chat.d.ts +3 -0
- package/dist/support/chat.d.ts.map +1 -0
- package/dist/support/chat.js +305 -0
- package/dist/support/chat.js.map +1 -0
- package/dist/support/chatBackend.d.ts +25 -0
- package/dist/support/chatBackend.d.ts.map +1 -0
- package/dist/support/chatBackend.js +289 -0
- package/dist/support/chatBackend.js.map +1 -0
- package/dist/support/chatTui.d.ts +3 -0
- package/dist/support/chatTui.d.ts.map +1 -0
- package/dist/support/chatTui.js +1082 -0
- package/dist/support/chatTui.js.map +1 -0
- package/dist/support/costTracker.d.ts +29 -0
- package/dist/support/costTracker.d.ts.map +1 -0
- package/dist/support/costTracker.js +113 -0
- package/dist/support/costTracker.js.map +1 -0
- package/dist/support/dashboardHtml.d.ts +5 -0
- package/dist/support/dashboardHtml.d.ts.map +1 -0
- package/dist/support/dashboardHtml.js +2629 -0
- package/dist/support/dashboardHtml.js.map +1 -0
- package/dist/support/dev.d.ts +55 -0
- package/dist/support/dev.d.ts.map +1 -0
- package/dist/support/dev.js +298 -0
- package/dist/support/dev.js.map +1 -0
- package/dist/support/editParser.d.ts +37 -0
- package/dist/support/editParser.d.ts.map +1 -0
- package/dist/support/editParser.js +365 -0
- package/dist/support/editParser.js.map +1 -0
- package/dist/support/ghosttyThemeCatalog.generated.d.ts +2 -0
- package/dist/support/ghosttyThemeCatalog.generated.d.ts.map +1 -0
- package/dist/support/ghosttyThemeCatalog.generated.js +11116 -0
- package/dist/support/ghosttyThemeCatalog.generated.js.map +1 -0
- package/dist/support/gitStatus.d.ts +21 -0
- package/dist/support/gitStatus.d.ts.map +1 -0
- package/dist/support/gitStatus.js +108 -0
- package/dist/support/gitStatus.js.map +1 -0
- package/dist/support/gitTracker.d.ts +30 -0
- package/dist/support/gitTracker.d.ts.map +1 -0
- package/dist/support/gitTracker.js +143 -0
- package/dist/support/gitTracker.js.map +1 -0
- package/dist/support/index.d.ts +12 -0
- package/dist/support/index.d.ts.map +1 -0
- package/dist/support/index.js +12 -0
- package/dist/support/index.js.map +1 -0
- package/dist/support/planner.d.ts +64 -0
- package/dist/support/planner.d.ts.map +1 -0
- package/dist/support/planner.js +396 -0
- package/dist/support/planner.js.map +1 -0
- package/dist/support/projectMapper.d.ts +46 -0
- package/dist/support/projectMapper.d.ts.map +1 -0
- package/dist/support/projectMapper.js +273 -0
- package/dist/support/projectMapper.js.map +1 -0
- package/dist/support/pty-helper.py +117 -0
- package/dist/support/quotaTracker.d.ts +29 -0
- package/dist/support/quotaTracker.d.ts.map +1 -0
- package/dist/support/quotaTracker.js +89 -0
- package/dist/support/quotaTracker.js.map +1 -0
- package/dist/support/rateLimiter.d.ts +101 -0
- package/dist/support/rateLimiter.d.ts.map +1 -0
- package/dist/support/rateLimiter.js +219 -0
- package/dist/support/rateLimiter.js.map +1 -0
- package/dist/support/rollback.d.ts +61 -0
- package/dist/support/rollback.d.ts.map +1 -0
- package/dist/support/rollback.js +329 -0
- package/dist/support/rollback.js.map +1 -0
- package/dist/support/sharedShell.d.ts +17 -0
- package/dist/support/sharedShell.d.ts.map +1 -0
- package/dist/support/sharedShell.js +439 -0
- package/dist/support/sharedShell.js.map +1 -0
- package/dist/support/stuckDetector.d.ts +68 -0
- package/dist/support/stuckDetector.d.ts.map +1 -0
- package/dist/support/stuckDetector.js +174 -0
- package/dist/support/stuckDetector.js.map +1 -0
- package/dist/support/terminalBridge.d.ts +18 -0
- package/dist/support/terminalBridge.d.ts.map +1 -0
- package/dist/support/terminalBridge.js +553 -0
- package/dist/support/terminalBridge.js.map +1 -0
- package/dist/support/timeWindow.d.ts +60 -0
- package/dist/support/timeWindow.d.ts.map +1 -0
- package/dist/support/timeWindow.js +236 -0
- package/dist/support/timeWindow.js.map +1 -0
- package/dist/support/uiThemes.d.ts +44 -0
- package/dist/support/uiThemes.d.ts.map +1 -0
- package/dist/support/uiThemes.js +290 -0
- package/dist/support/uiThemes.js.map +1 -0
- package/dist/support/web.d.ts +29 -0
- package/dist/support/web.d.ts.map +1 -0
- package/dist/support/web.js +1097 -0
- package/dist/support/web.js.map +1 -0
- package/dist/support/worktreeManager.d.ts +20 -0
- package/dist/support/worktreeManager.d.ts.map +1 -0
- package/dist/support/worktreeManager.js +140 -0
- package/dist/support/worktreeManager.js.map +1 -0
- package/dist/task_state_model.py +55 -0
- package/dist/workItemState/store.d.ts +122 -0
- package/dist/workItemState/store.d.ts.map +1 -0
- package/dist/workItemState/store.js +438 -0
- package/dist/workItemState/store.js.map +1 -0
- package/dist/workItems/azdoBridge.d.ts +42 -0
- package/dist/workItems/azdoBridge.d.ts.map +1 -0
- package/dist/workItems/azdoBridge.js +143 -0
- package/dist/workItems/azdoBridge.js.map +1 -0
- package/dist/workItems/azdoSyncRuntime.d.ts +28 -0
- package/dist/workItems/azdoSyncRuntime.d.ts.map +1 -0
- package/dist/workItems/azdoSyncRuntime.js +158 -0
- package/dist/workItems/azdoSyncRuntime.js.map +1 -0
- package/dist/workItems/azureDevOpsSync.d.ts +128 -0
- package/dist/workItems/azureDevOpsSync.d.ts.map +1 -0
- package/dist/workItems/azureDevOpsSync.js +748 -0
- package/dist/workItems/azureDevOpsSync.js.map +1 -0
- package/dist/workItems/helpers.d.ts +11 -0
- package/dist/workItems/helpers.d.ts.map +1 -0
- package/dist/workItems/helpers.js +17 -0
- package/dist/workItems/helpers.js.map +1 -0
- package/dist/workItems/index.d.ts +21 -0
- package/dist/workItems/index.d.ts.map +1 -0
- package/dist/workItems/index.js +89 -0
- package/dist/workItems/index.js.map +1 -0
- package/dist/workItems/localWorkItemFetcher.d.ts +55 -0
- package/dist/workItems/localWorkItemFetcher.d.ts.map +1 -0
- package/dist/workItems/localWorkItemFetcher.js +209 -0
- package/dist/workItems/localWorkItemFetcher.js.map +1 -0
- package/dist/workItems/migrations/001_rename_workItem_to_work_item.sql +10 -0
- package/dist/workItems/postgresStore.d.ts +78 -0
- package/dist/workItems/postgresStore.d.ts.map +1 -0
- package/dist/workItems/postgresStore.js +937 -0
- package/dist/workItems/postgresStore.js.map +1 -0
- package/dist/workItems/schema.d.ts +257 -0
- package/dist/workItems/schema.d.ts.map +1 -0
- package/dist/workItems/schema.js +176 -0
- package/dist/workItems/schema.js.map +1 -0
- package/dist/workItems/sqliteStore.d.ts +124 -0
- package/dist/workItems/sqliteStore.d.ts.map +1 -0
- package/dist/workItems/sqliteStore.js +713 -0
- package/dist/workItems/sqliteStore.js.map +1 -0
- package/dist/workItems/workItemBoardHtml.d.ts +5 -0
- package/dist/workItems/workItemBoardHtml.d.ts.map +1 -0
- package/dist/workItems/workItemBoardHtml.js +2192 -0
- package/dist/workItems/workItemBoardHtml.js.map +1 -0
- package/package.json +99 -0
- package/templates/AGENTS.md +432 -0
- package/templates/BOOT.md +25 -0
- package/templates/BOOTSTRAP.md +50 -0
- package/templates/CHANGELOG_AUDIT.md +74 -0
- package/templates/HEARTBEAT.md +86 -0
- package/templates/IDENTITY.md +27 -0
- package/templates/PR_LAND.md +75 -0
- package/templates/PR_REVIEW.md +97 -0
- package/templates/SOUL.dev.md +52 -0
- package/templates/SOUL.md +81 -0
- package/templates/TOOLS.md +52 -0
- package/templates/USER.md +22 -0
- package/templates/WORKITEM_ANALYSIS.md +31 -0
- package/templates/agents/executor.md +26 -0
- package/templates/agents/plan.md +22 -0
- package/templates/agents/ralph.md +37 -0
- package/templates/agents/review.md +22 -0
- package/templates/agents/team.md +39 -0
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// EnactFactory - Azure DevOps Heartbeat Sync Orchestrator
|
|
3
|
+
// Pull-down from AzDO, push-up local changes, stale detection, SSE events.
|
|
4
|
+
// ============================================
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { AzdoApiError, createAzdoClient } from '../azureDevOps/client.js';
|
|
8
|
+
import { DEFAULT_FIELD_NAMES, azdoToWorkItemRecord, azdoToWorkItemPayload, payloadToAzdoPatches, azdoToArea } from '../azureDevOps/mapper.js';
|
|
9
|
+
import { loadConfig } from '../core/config.js';
|
|
10
|
+
import { provisionHierarchy } from '../azureDevOps/hierarchy.js';
|
|
11
|
+
import { DEFAULT_AZDO_TERMINAL_STATES, localStatusToAzdoState, resolveTerminalAzdoStates, } from '../azureDevOps/stateMapping.js';
|
|
12
|
+
import { resolveProjectConfigs } from '../core/runtimeConfig.js';
|
|
13
|
+
import { EventEmitter } from 'node:events';
|
|
14
|
+
import { drainPushQueue } from '../factory/workitemQueues.js';
|
|
15
|
+
const execFileAsync = promisify(execFile);
|
|
16
|
+
/**
|
|
17
|
+
* Drains the workitem-push-queue at `root` by calling pushLocalChange for
|
|
18
|
+
* each pending entry. Entries that succeed are marked drained. Entries that
|
|
19
|
+
* exhaust all attempts remain in the queue and are reported in result.errors.
|
|
20
|
+
*
|
|
21
|
+
* Exponential backoff: delay = backoffMs * 2^attempt (capped at 30 000 ms).
|
|
22
|
+
*/
|
|
23
|
+
export async function drainWorkItemPushQueue(root, context, options = {}) {
|
|
24
|
+
const maxAttempts = options.maxAttemptsPerEntry ?? 3;
|
|
25
|
+
const backoffMs = options.backoffMs ?? 1000;
|
|
26
|
+
const errors = [];
|
|
27
|
+
// Build an inline pushLocalChange equivalent using the provided context.
|
|
28
|
+
const { client, store, config, events } = context;
|
|
29
|
+
const fieldNames = { ...DEFAULT_FIELD_NAMES, ...config.fieldNames };
|
|
30
|
+
async function pushEntry(workItemId) {
|
|
31
|
+
const local = await store.getWorkItem(workItemId);
|
|
32
|
+
if (!local) {
|
|
33
|
+
console.warn(`[AzdoSync] drainPushQueue: item ${workItemId} not found in local store — skipping`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const numericId = parseInt(workItemId, 10);
|
|
37
|
+
if (isNaN(numericId)) {
|
|
38
|
+
console.warn(`[AzdoSync] drainPushQueue: ${workItemId} is not a numeric AzDO ID — skipping`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Source the patch from local intent (stored payload + local title), not a
|
|
42
|
+
// fresh AzDO fetch — otherwise we re-derive from AzDO's current state and
|
|
43
|
+
// clobber the local edit we're draining.
|
|
44
|
+
const patches = await buildLocalPushPatches(store, fieldNames, workItemId, local, context.config);
|
|
45
|
+
try {
|
|
46
|
+
await withExponentialBackoff(() => client.updateWorkItem(numericId, patches), 3, 1000, 30000);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
if (err instanceof AzdoApiError && err.status !== 429 && err.status !== 503) {
|
|
50
|
+
console.error(`[AzdoSync] drainPushQueue push failed for ${workItemId}:`, err instanceof Error ? err.message : String(err));
|
|
51
|
+
events.emit('azdo.push.failed', { workItemId, status: err.status, message: err.message });
|
|
52
|
+
// Non-retriable — surface as resolved so we don't re-queue indefinitely.
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const queueResult = await drainPushQueue(root, async (entry) => {
|
|
59
|
+
let lastErr;
|
|
60
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
61
|
+
try {
|
|
62
|
+
await pushEntry(entry.workItemId);
|
|
63
|
+
return; // success → drainPushQueue marks this entry drained
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
lastErr = err;
|
|
67
|
+
if (attempt < maxAttempts - 1) {
|
|
68
|
+
await new Promise((r) => setTimeout(r, backoffMs * Math.pow(2, attempt)));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// All attempts exhausted — record for forensics and rethrow so
|
|
73
|
+
// drainPushQueue does NOT mark this entry drained.
|
|
74
|
+
errors.push({
|
|
75
|
+
entryId: entry.id,
|
|
76
|
+
workItemId: entry.workItemId,
|
|
77
|
+
attempts: maxAttempts,
|
|
78
|
+
lastError: lastErr instanceof Error ? lastErr.message : String(lastErr),
|
|
79
|
+
});
|
|
80
|
+
throw lastErr;
|
|
81
|
+
});
|
|
82
|
+
return { drained: queueResult.drained, errors };
|
|
83
|
+
}
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Internal helpers
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
const BATCH_SIZE = 200;
|
|
88
|
+
// Default Scrum terminal states — open = not in these. PBI/Bug (the Scrum
|
|
89
|
+
// process template) end their lifecycle at exactly 'Done' or 'Removed'.
|
|
90
|
+
// Overridable per-workspace via AzdoSyncConfig.terminalStates.
|
|
91
|
+
export const AZDO_TERMINAL_STATES = DEFAULT_AZDO_TERMINAL_STATES;
|
|
92
|
+
function buildWiql(project, areaFilter, changedSince, terminalStates = AZDO_TERMINAL_STATES) {
|
|
93
|
+
// Board scope: only executable work items (Product Backlog Item, Bug).
|
|
94
|
+
// Epic/Feature are planning containers — they live in AzDO for hierarchy
|
|
95
|
+
// and breadcrumbs but are out of scope for the Factory board.
|
|
96
|
+
let wiql = `SELECT [System.Id] FROM WorkItems` +
|
|
97
|
+
` WHERE [System.TeamProject] = '${project}'` +
|
|
98
|
+
` AND [System.WorkItemType] IN ('Product Backlog Item', 'Bug')`;
|
|
99
|
+
// Open items only — exclude terminal states so the fetch stays bounded at
|
|
100
|
+
// scale and the board mirrors only live work. Applied in both modes.
|
|
101
|
+
const terminalList = terminalStates.map((s) => `'${s}'`).join(', ');
|
|
102
|
+
wiql += ` AND [System.State] NOT IN (${terminalList})`;
|
|
103
|
+
if (areaFilter) {
|
|
104
|
+
wiql += ` AND [System.AreaPath] UNDER '${areaFilter}'`;
|
|
105
|
+
}
|
|
106
|
+
if (changedSince) {
|
|
107
|
+
// Incremental mode: only items touched since the watermark, ordered by
|
|
108
|
+
// ChangedDate ASC so the watermark can advance monotonically.
|
|
109
|
+
//
|
|
110
|
+
// AzDO WIQL requires DATE precision for [System.ChangedDate] comparisons —
|
|
111
|
+
// supplying a full ISO timestamp (e.g. "2026-06-16T00:24:05.860Z") causes
|
|
112
|
+
// HTTP 400: "You cannot supply a time with the date when running a query
|
|
113
|
+
// using date precision". Truncate to YYYY-MM-DD.
|
|
114
|
+
//
|
|
115
|
+
// Date-precision re-scans from midnight of the watermark's date, so items
|
|
116
|
+
// changed earlier the same day are re-fetched on the next tick. That is
|
|
117
|
+
// safe: mergeRecord is idempotent (no-op when local is already current) and
|
|
118
|
+
// the stored watermark stays full-ISO so the monotonic advance is preserved.
|
|
119
|
+
const azdoDate = new Date(changedSince).toISOString().slice(0, 10);
|
|
120
|
+
wiql += ` AND [System.ChangedDate] >= '${azdoDate}'`;
|
|
121
|
+
wiql += ' ORDER BY [System.ChangedDate] ASC';
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// Full mode: stable id ordering over the complete set.
|
|
125
|
+
wiql += ' ORDER BY [System.Id]';
|
|
126
|
+
}
|
|
127
|
+
return wiql;
|
|
128
|
+
}
|
|
129
|
+
async function sleep(ms) {
|
|
130
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
131
|
+
}
|
|
132
|
+
function extractParentId(workItem) {
|
|
133
|
+
const relation = workItem.relations?.find((entry) => entry.rel === 'System.LinkTypes.Hierarchy-Reverse');
|
|
134
|
+
if (!relation?.url)
|
|
135
|
+
return null;
|
|
136
|
+
const match = relation.url.match(/workItems\/(\d+)/i);
|
|
137
|
+
return match ? match[1] : null;
|
|
138
|
+
}
|
|
139
|
+
async function withExponentialBackoff(fn, maxAttempts, baseDelayMs, capMs) {
|
|
140
|
+
let delay = baseDelayMs;
|
|
141
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
142
|
+
try {
|
|
143
|
+
return await fn();
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
if (attempt === maxAttempts)
|
|
147
|
+
throw err;
|
|
148
|
+
// Only retry on 429 / 503
|
|
149
|
+
if (err instanceof AzdoApiError && err.status !== 429 && err.status !== 503) {
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
await sleep(Math.min(delay, capMs));
|
|
153
|
+
delay *= 2;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Unreachable — satisfies TS
|
|
157
|
+
throw new Error('withExponentialBackoff: exhausted attempts');
|
|
158
|
+
}
|
|
159
|
+
function mergeRecord(local, azdoRecord, azdoChangedDate, payloadFields) {
|
|
160
|
+
// Conflict rule: if local was updated after AzDO's changedDate, keep local for AzDO-owned fields
|
|
161
|
+
const localUpdated = new Date(local.updatedAt).getTime();
|
|
162
|
+
const azdoChanged = new Date(azdoChangedDate).getTime();
|
|
163
|
+
if (localUpdated > azdoChanged) {
|
|
164
|
+
// Local is newer — keep it as-is
|
|
165
|
+
return { merged: {}, changed: false };
|
|
166
|
+
}
|
|
167
|
+
// AzDO is newer — accept AzDO-owned fields
|
|
168
|
+
const patch = {};
|
|
169
|
+
let changed = false;
|
|
170
|
+
// Flat fields that azdoToWorkItemRecord produces.
|
|
171
|
+
const fields = [
|
|
172
|
+
'title', 'description', 'status', 'priority',
|
|
173
|
+
];
|
|
174
|
+
for (const field of fields) {
|
|
175
|
+
const azdoVal = azdoRecord[field];
|
|
176
|
+
if (azdoVal !== undefined && local[field] !== azdoVal) {
|
|
177
|
+
patch[field] = azdoVal;
|
|
178
|
+
changed = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Payload-derived fields (area, kind, assignee) — these ARE DB columns but are
|
|
182
|
+
// not produced by azdoToWorkItemRecord. Merge them here when AzDO is canonical.
|
|
183
|
+
if (payloadFields) {
|
|
184
|
+
// area: merge only when AzDO has a real sub-area (non-null). A null area
|
|
185
|
+
// means the item lives at the project root — don't clobber an existing local area.
|
|
186
|
+
if (payloadFields.area !== null && payloadFields.area !== undefined &&
|
|
187
|
+
local.area !== payloadFields.area) {
|
|
188
|
+
patch.area = payloadFields.area;
|
|
189
|
+
changed = true;
|
|
190
|
+
}
|
|
191
|
+
// kind: always present on synced PBIs/Bugs; update if it changed upstream.
|
|
192
|
+
if (payloadFields.kind !== undefined && local.kind !== payloadFields.kind) {
|
|
193
|
+
patch.kind = payloadFields.kind;
|
|
194
|
+
changed = true;
|
|
195
|
+
}
|
|
196
|
+
// assignee: null means unassigned — propagate that too (clear the local assignee).
|
|
197
|
+
const azdoAssignee = payloadFields.assignee ?? null;
|
|
198
|
+
const localAssignee = local.assignee ?? null;
|
|
199
|
+
if (localAssignee !== azdoAssignee) {
|
|
200
|
+
// Use explicit `null` (not `undefined`) so updateWorkItem writes NULL and
|
|
201
|
+
// actually clears the column. `undefined` would be treated as "leave unchanged"
|
|
202
|
+
// by the Postgres store (and dropped by the `{...existing, ...patch}` spread),
|
|
203
|
+
// so the local assignee would never clear when AzDO unassigns the item.
|
|
204
|
+
patch['assignee'] = azdoAssignee;
|
|
205
|
+
changed = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (changed) {
|
|
209
|
+
// Stamp the REAL AzDO ChangedDate, not NOW() — otherwise every merged row
|
|
210
|
+
// looks "just now" on the dashboard even though AzDO changed it minutes ago.
|
|
211
|
+
patch.updatedAt = azdoChangedDate;
|
|
212
|
+
}
|
|
213
|
+
return { merged: patch, changed };
|
|
214
|
+
}
|
|
215
|
+
// Canonical assignee re-applied on every drained push (mirrors azdoBridge.ts).
|
|
216
|
+
// Replicated inline — importing from azdoBridge would create an import cycle.
|
|
217
|
+
const AZDO_DEFAULT_ASSIGNEE = 'Tarun Rana <tarun.rana@amsterdamdatalabs.com>';
|
|
218
|
+
/**
|
|
219
|
+
* Build AzDO patches to push a LOCAL change up, sourced from local intent
|
|
220
|
+
* rather than a fresh AzDO fetch. Re-fetching AzDO and re-deriving patches
|
|
221
|
+
* from its current state clobbers the very edit we're trying to push.
|
|
222
|
+
*
|
|
223
|
+
* Strategy: take the last-known stored payload (which carries the
|
|
224
|
+
* AzDO-canonical area/kind from the previous pull), overlay the local record's
|
|
225
|
+
* authoritative title, then OVERRIDE System.State with the value derived from
|
|
226
|
+
* `local.status`. The stored payload's layeredState is the PRE-edit state, so
|
|
227
|
+
* for a queued status change (e.g. a failed soft-delete cancel) trusting it
|
|
228
|
+
* would push a stale non-Removed state and the delete would not stick — the
|
|
229
|
+
* row resurrects on the next full pull. We therefore de-dup any System.State
|
|
230
|
+
* patch the payload produced and append exactly one built from local.status.
|
|
231
|
+
* AssignedTo is (re)applied to honor the same canonical-assignee guarantee as
|
|
232
|
+
* the direct bridge pushes.
|
|
233
|
+
*/
|
|
234
|
+
async function buildLocalPushPatches(store, fieldNames, workItemId, local, syncConfig) {
|
|
235
|
+
// Base patches: payload-derived (title/area/booleans) or title-only fallback.
|
|
236
|
+
let base;
|
|
237
|
+
const storedJson = await store.getPayloadJson(workItemId);
|
|
238
|
+
let parsed = null;
|
|
239
|
+
if (storedJson) {
|
|
240
|
+
try {
|
|
241
|
+
parsed = JSON.parse(storedJson);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Corrupt stored payload — fall through to the title-only fallback.
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (parsed) {
|
|
248
|
+
const overlaid = { ...parsed, title: local.title };
|
|
249
|
+
base = payloadToAzdoPatches(overlaid, fieldNames);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
base = [{ op: 'replace', path: `/fields/${fieldNames.title}`, value: local.title }];
|
|
253
|
+
}
|
|
254
|
+
// Drop any System.State patch the payload emitted — its value is the stale
|
|
255
|
+
// pre-edit state. The authoritative state comes from local.status below.
|
|
256
|
+
const statePath = `/fields/${fieldNames.state}`;
|
|
257
|
+
base = base.filter((p) => p.path !== statePath);
|
|
258
|
+
// Append the single authoritative System.State + the canonical AssignedTo.
|
|
259
|
+
base.push({ op: 'add', path: statePath, value: localStatusToAzdoState(local.status, syncConfig) });
|
|
260
|
+
base.push({ op: 'add', path: `/fields/${fieldNames.assignedTo}`, value: AZDO_DEFAULT_ASSIGNEE });
|
|
261
|
+
return base;
|
|
262
|
+
}
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// fetchToken factories (exported for service.ts bootstrap)
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
export function makePATFetcher(patEnvVar) {
|
|
267
|
+
return async () => {
|
|
268
|
+
const pat = process.env[patEnvVar];
|
|
269
|
+
if (!pat) {
|
|
270
|
+
throw new Error(`AzDO PAT environment variable "${patEnvVar}" is not set`);
|
|
271
|
+
}
|
|
272
|
+
return `Basic ${Buffer.from(':' + pat).toString('base64')}`;
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
export function makeAzCliTokenFetcher(tenant) {
|
|
276
|
+
return async () => {
|
|
277
|
+
const args = [
|
|
278
|
+
'account', 'get-access-token',
|
|
279
|
+
'--resource', '499b84ac-1321-427f-aa17-267ca6975798',
|
|
280
|
+
];
|
|
281
|
+
if (tenant) {
|
|
282
|
+
args.push('--tenant', tenant);
|
|
283
|
+
}
|
|
284
|
+
args.push('--query', 'accessToken', '-o', 'tsv');
|
|
285
|
+
const { stdout } = await execFileAsync('az', args);
|
|
286
|
+
return stdout.trim();
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// AzdoSyncContext builder — shared by the service and MCP tool
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
/**
|
|
293
|
+
* Build an AzdoSyncContext from workspace config.
|
|
294
|
+
* Organization / project come from config.toml; PAT remains a secret env var.
|
|
295
|
+
*/
|
|
296
|
+
export function buildAzdoSyncContextFromConfig(store) {
|
|
297
|
+
const loadedConfig = loadConfig();
|
|
298
|
+
const azdoCfg = loadedConfig.azureDevOps;
|
|
299
|
+
if (!azdoCfg?.enabled) {
|
|
300
|
+
throw new Error('factory_workitem_drain_push_queue: Azure DevOps sync is disabled in config.toml');
|
|
301
|
+
}
|
|
302
|
+
const fetchToken = azdoCfg.authStrategy === 'pat'
|
|
303
|
+
? makePATFetcher(azdoCfg.patEnvVar ?? 'AZDO_PAT')
|
|
304
|
+
: makeAzCliTokenFetcher('amsterdamdatalabs.com');
|
|
305
|
+
const client = createAzdoClient({ organization: azdoCfg.organization, project: azdoCfg.project, fetchToken });
|
|
306
|
+
const events = new EventEmitter();
|
|
307
|
+
const syncConfig = {
|
|
308
|
+
enabled: true,
|
|
309
|
+
organization: azdoCfg.organization,
|
|
310
|
+
project: azdoCfg.project,
|
|
311
|
+
intervalMs: azdoCfg.intervalMs,
|
|
312
|
+
authStrategy: azdoCfg.authStrategy,
|
|
313
|
+
patEnvVar: azdoCfg.patEnvVar,
|
|
314
|
+
fieldNames: azdoCfg.fieldNames,
|
|
315
|
+
areaFilter: azdoCfg.areaFilter,
|
|
316
|
+
azdoStateToStatus: azdoCfg.azdoStateToStatus,
|
|
317
|
+
statusToAzdoState: azdoCfg.statusToAzdoState,
|
|
318
|
+
terminalStates: azdoCfg.terminalStates,
|
|
319
|
+
};
|
|
320
|
+
return { client, store, config: syncConfig, events, fetchToken };
|
|
321
|
+
}
|
|
322
|
+
export function createAzdoSyncService(deps, options) {
|
|
323
|
+
const { client, store, config, events } = deps;
|
|
324
|
+
const drainAfterSync = options?.drainPushQueueAfterSync !== false;
|
|
325
|
+
const root = options?.root;
|
|
326
|
+
const fieldNames = { ...DEFAULT_FIELD_NAMES, ...config.fieldNames };
|
|
327
|
+
// Incremental-sync watermark key. Scoped to org/project/areaFilter so a
|
|
328
|
+
// workspace that re-points at a different project/area starts fresh instead
|
|
329
|
+
// of inheriting a stale ChangedDate cursor.
|
|
330
|
+
const watermarkKey = `azdo:watermark:${config.organization}/${config.project}:${config.areaFilter ?? ''}`;
|
|
331
|
+
// Single source of truth for "what counts as terminal". Drives both the
|
|
332
|
+
// open-only WIQL filter and the reconcile's closed-item detection so they
|
|
333
|
+
// can't drift. Defaults to AZDO_TERMINAL_STATES when config omits it.
|
|
334
|
+
const terminalStates = resolveTerminalAzdoStates(config);
|
|
335
|
+
const terminalStateSet = new Set(terminalStates);
|
|
336
|
+
// State
|
|
337
|
+
let intervalHandle = null;
|
|
338
|
+
let inFlightSync = null;
|
|
339
|
+
let started = false;
|
|
340
|
+
let lastSuccessAt = null;
|
|
341
|
+
let lastErrorAt = null;
|
|
342
|
+
let lastError;
|
|
343
|
+
let wasStale = false;
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
// Stale detection
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
function checkStale() {
|
|
348
|
+
const now = Date.now();
|
|
349
|
+
const threshold = config.intervalMs * 3;
|
|
350
|
+
const isStale = lastSuccessAt === null || (now - lastSuccessAt) > threshold;
|
|
351
|
+
if (isStale && !wasStale) {
|
|
352
|
+
wasStale = true;
|
|
353
|
+
events.emit('azdo.sync.stale', { at: now });
|
|
354
|
+
}
|
|
355
|
+
else if (!isStale && wasStale) {
|
|
356
|
+
wasStale = false;
|
|
357
|
+
events.emit('azdo.sync.healthy', { at: now });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// syncOnce — pull-down
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
async function syncOnce(opts) {
|
|
364
|
+
const t0 = Date.now();
|
|
365
|
+
const result = { pulled: 0, pushed: 0, conflicts: 0, errors: [], durationMs: 0 };
|
|
366
|
+
const mode = opts?.mode ?? 'incremental';
|
|
367
|
+
try {
|
|
368
|
+
// Incremental mode reads the stored watermark and only pulls items whose
|
|
369
|
+
// System.ChangedDate is at or after it. The very first incremental run
|
|
370
|
+
// (no watermark yet) falls through to a full scan so we don't miss the
|
|
371
|
+
// existing backlog. Full mode always scans the complete id set.
|
|
372
|
+
const currentWatermark = await store.getSyncMeta(watermarkKey);
|
|
373
|
+
const effectiveMode = mode === 'incremental' && !currentWatermark ? 'full' : mode;
|
|
374
|
+
const changedSince = effectiveMode === 'incremental' ? currentWatermark ?? undefined : undefined;
|
|
375
|
+
const wiql = buildWiql(config.project, config.areaFilter, changedSince, terminalStates);
|
|
376
|
+
const ids = await client.queryByWiql(wiql);
|
|
377
|
+
// Batch IDs in groups of BATCH_SIZE
|
|
378
|
+
const batches = [];
|
|
379
|
+
for (let i = 0; i < ids.length; i += BATCH_SIZE) {
|
|
380
|
+
batches.push(ids.slice(i, i + BATCH_SIZE));
|
|
381
|
+
}
|
|
382
|
+
// Track every AzDO id we see this heartbeat so we can prune local
|
|
383
|
+
// rows whose upstream item has been deleted in AzDO. Prune only runs in
|
|
384
|
+
// full mode (incremental never sees the complete id set).
|
|
385
|
+
const azdoIdsSeen = new Set();
|
|
386
|
+
// Highest System.ChangedDate observed across processed items this run.
|
|
387
|
+
// After a successful run we advance the watermark to (max − 60s) so a
|
|
388
|
+
// small clock-skew window is re-scanned next time; re-processing is
|
|
389
|
+
// idempotent because create/merge key on the AzDO id PK.
|
|
390
|
+
let maxChangedMs = 0;
|
|
391
|
+
// ALL open executable items (PBI/Bug) this run, each carrying its CURRENT
|
|
392
|
+
// upstream parent id. Provisioning self-heals wrong parentage, so it needs
|
|
393
|
+
// every executable item (not just empty-parent orphans) to detect and fix
|
|
394
|
+
// mis-parented ones. Collected in full mode only; provisioned after the
|
|
395
|
+
// hydration loop + reconcile.
|
|
396
|
+
const hierItems = [];
|
|
397
|
+
for (const batch of batches) {
|
|
398
|
+
const azdoItems = await client.getWorkItems(batch);
|
|
399
|
+
const parentIds = Array.from(new Set(azdoItems
|
|
400
|
+
.map((item) => extractParentId(item))
|
|
401
|
+
.filter((id) => Boolean(id))));
|
|
402
|
+
const parentItems = parentIds.length > 0
|
|
403
|
+
? await client.getWorkItems(parentIds.map((id) => Number(id)))
|
|
404
|
+
: [];
|
|
405
|
+
const parentMap = new Map(parentItems.map((item) => [String(item.id), item]));
|
|
406
|
+
const grandparentIds = Array.from(new Set(parentItems
|
|
407
|
+
.map((item) => extractParentId(item))
|
|
408
|
+
.filter((id) => Boolean(id))));
|
|
409
|
+
const grandparentItems = grandparentIds.length > 0
|
|
410
|
+
? await client.getWorkItems(grandparentIds.map((id) => Number(id)))
|
|
411
|
+
: [];
|
|
412
|
+
const grandparentMap = new Map(grandparentItems.map((item) => [String(item.id), item]));
|
|
413
|
+
for (const azdo of azdoItems) {
|
|
414
|
+
const itemId = String(azdo.id);
|
|
415
|
+
azdoIdsSeen.add(itemId);
|
|
416
|
+
try {
|
|
417
|
+
const azdoRecord = azdoToWorkItemRecord(azdo, config);
|
|
418
|
+
const azdoChangedDate = String(azdo.fields['System.ChangedDate'] ?? new Date().toISOString());
|
|
419
|
+
const changedMs = new Date(azdoChangedDate).getTime();
|
|
420
|
+
if (Number.isFinite(changedMs) && changedMs > maxChangedMs) {
|
|
421
|
+
maxChangedMs = changedMs;
|
|
422
|
+
}
|
|
423
|
+
const payload = azdoToWorkItemPayload(azdo, fieldNames, config);
|
|
424
|
+
const parentId = extractParentId(azdo);
|
|
425
|
+
// Collect EVERY open executable item (Backlog item / Bug) for the
|
|
426
|
+
// hierarchy self-heal (full mode only), carrying its CURRENT parent
|
|
427
|
+
// id (null if none). Provisioning needs all of them — not just
|
|
428
|
+
// empty-parent orphans — to detect and re-parent mis-parented items.
|
|
429
|
+
// Epics/Features are never returned by the WIQL, but we still guard
|
|
430
|
+
// on kind. kind/area come from the payload/mapper (the record mapper
|
|
431
|
+
// doesn't emit them); area is the real value or omitted (no fabrication).
|
|
432
|
+
if (effectiveMode === 'full' &&
|
|
433
|
+
(payload.kind === 'Backlog item' || payload.kind === 'Bug')) {
|
|
434
|
+
const itemArea = azdoToArea(azdo);
|
|
435
|
+
hierItems.push({
|
|
436
|
+
id: itemId,
|
|
437
|
+
kind: payload.kind,
|
|
438
|
+
...(itemArea !== null ? { area: itemArea } : {}),
|
|
439
|
+
currentParentId: parentId,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
const parent = parentId ? parentMap.get(parentId) : null;
|
|
443
|
+
const grandparentId = parent ? extractParentId(parent) : null;
|
|
444
|
+
const grandparent = grandparentId ? grandparentMap.get(grandparentId) : null;
|
|
445
|
+
payload.parentId = parentId;
|
|
446
|
+
if (payload.azdoFields) {
|
|
447
|
+
payload.azdoFields.parentId = parentId;
|
|
448
|
+
payload.azdoFields.parentTitle = parent ? String(parent.fields[fieldNames.title] ?? '') || null : null;
|
|
449
|
+
payload.azdoFields.parentWorkItemType = parent ? String(parent.fields[fieldNames.workItemType] ?? '') || null : null;
|
|
450
|
+
payload.azdoFields.parentStartDate = parent ? String(parent.fields[fieldNames.startDate] ?? '') || null : null;
|
|
451
|
+
payload.azdoFields.parentTargetDate = parent ? String(parent.fields[fieldNames.targetDate] ?? '') || null : null;
|
|
452
|
+
payload.azdoFields.grandparentId = grandparentId;
|
|
453
|
+
payload.azdoFields.grandparentTitle = grandparent ? String(grandparent.fields[fieldNames.title] ?? '') || null : null;
|
|
454
|
+
payload.azdoFields.grandparentWorkItemType = grandparent ? String(grandparent.fields[fieldNames.workItemType] ?? '') || null : null;
|
|
455
|
+
payload.azdoFields.grandparentStartDate = grandparent ? String(grandparent.fields[fieldNames.startDate] ?? '') || null : null;
|
|
456
|
+
payload.azdoFields.grandparentTargetDate = grandparent ? String(grandparent.fields[fieldNames.targetDate] ?? '') || null : null;
|
|
457
|
+
}
|
|
458
|
+
const payloadJson = JSON.stringify(payload);
|
|
459
|
+
const local = await store.getWorkItem(itemId);
|
|
460
|
+
if (!local) {
|
|
461
|
+
// Insert with the AzDO numeric id as the local PK; tag as 'azdo'
|
|
462
|
+
// so future heartbeats and pruning can identify mirrored rows.
|
|
463
|
+
//
|
|
464
|
+
// Forward the REAL AzDO values for every content field the store
|
|
465
|
+
// now persists. Phase 1 removed the store's fabricated defaults,
|
|
466
|
+
// so anything we omit lands as NULL. createdAt/updatedAt come from
|
|
467
|
+
// the AzDO record (real System.CreatedDate / System.ChangedDate) —
|
|
468
|
+
// omitting them used to make the store fall back to NOW(), which is
|
|
469
|
+
// exactly the "just now" dashboard bug.
|
|
470
|
+
//
|
|
471
|
+
// kind/area/review/resolution/assignee are not on azdoRecord (the
|
|
472
|
+
// record mapper only covers the flat columns) — derive them from
|
|
473
|
+
// the payload/mapper helpers. `area` is taken from azdoToArea so a
|
|
474
|
+
// genuinely root-only path stays null rather than being fabricated
|
|
475
|
+
// as 'Enact/Factory'.
|
|
476
|
+
const area = azdoToArea(azdo);
|
|
477
|
+
await store.createWorkItem({
|
|
478
|
+
id: itemId,
|
|
479
|
+
projectId: azdoRecord.projectId ?? config.project,
|
|
480
|
+
title: azdoRecord.title ?? '',
|
|
481
|
+
description: azdoRecord.description,
|
|
482
|
+
status: azdoRecord.status,
|
|
483
|
+
priority: azdoRecord.priority ?? undefined,
|
|
484
|
+
source: 'azdo',
|
|
485
|
+
kind: payload.kind,
|
|
486
|
+
...(area !== null ? { area } : {}),
|
|
487
|
+
reviewState: payload.layeredState.review,
|
|
488
|
+
resolution: payload.layeredState.resolution,
|
|
489
|
+
...(payload.azdoFields?.assignedTo ? { assignee: payload.azdoFields.assignedTo } : {}),
|
|
490
|
+
createdAt: azdoRecord.createdAt,
|
|
491
|
+
updatedAt: azdoChangedDate,
|
|
492
|
+
...(azdoRecord.closedAt !== undefined ? { closedAt: azdoRecord.closedAt } : {}),
|
|
493
|
+
});
|
|
494
|
+
await store.setPayloadJson(itemId, payloadJson);
|
|
495
|
+
result.pulled++;
|
|
496
|
+
events.emit('workitem.updated', { id: itemId, action: 'created' });
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
const { merged, changed } = mergeRecord(local, azdoRecord, azdoChangedDate, {
|
|
500
|
+
area: azdoToArea(azdo),
|
|
501
|
+
kind: payload.kind,
|
|
502
|
+
assignee: payload.azdoFields?.assignedTo ?? null,
|
|
503
|
+
});
|
|
504
|
+
if (changed) {
|
|
505
|
+
await store.updateWorkItem(itemId, merged);
|
|
506
|
+
result.pulled++;
|
|
507
|
+
events.emit('workitem.updated', { id: itemId, action: 'updated' });
|
|
508
|
+
}
|
|
509
|
+
else if (new Date(local.updatedAt).getTime() > new Date(azdoChangedDate).getTime()) {
|
|
510
|
+
result.conflicts++;
|
|
511
|
+
}
|
|
512
|
+
// Always refresh the payload mirror — even if record fields
|
|
513
|
+
// didn't change, AzDO custom fields (operator lane, lifecycle
|
|
514
|
+
// booleans, etc.) may have been edited upstream.
|
|
515
|
+
await store.setPayloadJson(itemId, payloadJson);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
catch (err) {
|
|
519
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
520
|
+
result.errors.push({ workItemId: itemId, message: msg });
|
|
521
|
+
console.warn(`[AzdoSync] Error processing item ${itemId}: ${msg}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// Reconcile AzDO-sourced rows that are open locally but absent from the
|
|
526
|
+
// open upstream set — full mode only. Incremental never queries the
|
|
527
|
+
// complete id set, so its "not returned" set would be every unchanged
|
|
528
|
+
// item (heavy + wrong). Rather than blind-deleting (the old pruneOrphans
|
|
529
|
+
// behavior), we RE-FETCH only the small diff to confirm true state before
|
|
530
|
+
// removing anything — a transient fetch error must never cause an
|
|
531
|
+
// erroneous prune.
|
|
532
|
+
if (effectiveMode === 'full') {
|
|
533
|
+
const TERMINAL_LOCAL_STATUSES = new Set(['done', 'cancelled', 'duplicate']);
|
|
534
|
+
// Local OPEN azdo rows: source='azdo' and not in a terminal local
|
|
535
|
+
// status. Page through the LOCAL store (fixed page size, advance offset
|
|
536
|
+
// until a short page) so a board with >1 page of open rows can't
|
|
537
|
+
// silently skip prune candidates. This read is local-only; only the
|
|
538
|
+
// candidate diff is later fetched from AzDO.
|
|
539
|
+
const LOCAL_PAGE_SIZE = 1000;
|
|
540
|
+
const localOpenIds = [];
|
|
541
|
+
for (let offset = 0;; offset += LOCAL_PAGE_SIZE) {
|
|
542
|
+
const page = await store.listWorkItems({ source: 'azdo', limit: LOCAL_PAGE_SIZE, offset });
|
|
543
|
+
for (const r of page.workItems) {
|
|
544
|
+
if (!TERMINAL_LOCAL_STATUSES.has(r.status)) {
|
|
545
|
+
localOpenIds.push(r.id);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (page.workItems.length < LOCAL_PAGE_SIZE)
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
// Candidates: open locally but NOT in the open upstream set this run.
|
|
552
|
+
const candidates = localOpenIds.filter((id) => !azdoIdsSeen.has(id));
|
|
553
|
+
let prunedCount = 0;
|
|
554
|
+
if (candidates.length > 0) {
|
|
555
|
+
for (let i = 0; i < candidates.length; i += BATCH_SIZE) {
|
|
556
|
+
const batch = candidates.slice(i, i + BATCH_SIZE);
|
|
557
|
+
let fetched;
|
|
558
|
+
try {
|
|
559
|
+
fetched = await withExponentialBackoff(() => client.getWorkItems(batch.map(Number)), 3, 1000, 30000);
|
|
560
|
+
}
|
|
561
|
+
catch (err) {
|
|
562
|
+
// Transient fetch failure — KEEP this batch (safer than deleting
|
|
563
|
+
// on an error). The next reconcile retries it.
|
|
564
|
+
console.warn(`[AzdoSync] Reconcile fetch failed for ${batch.length} candidate(s) — keeping them: ` +
|
|
565
|
+
(err instanceof Error ? err.message : String(err)));
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
const fetchedMap = new Map(fetched.map((item) => [String(item.id), item]));
|
|
569
|
+
for (const id of batch) {
|
|
570
|
+
const item = fetchedMap.get(id);
|
|
571
|
+
if (!item) {
|
|
572
|
+
// Hard-deleted upstream — confirmed gone.
|
|
573
|
+
await store.deleteWorkItem(id);
|
|
574
|
+
prunedCount++;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
// Present upstream: read its true System.State. Terminal check
|
|
578
|
+
// uses the SAME configured set as the WIQL open-filter so the two
|
|
579
|
+
// can't drift.
|
|
580
|
+
const upstreamState = String(item.fields[fieldNames.state] ?? '').trim();
|
|
581
|
+
const isTerminal = terminalStateSet.has(upstreamState);
|
|
582
|
+
if (isTerminal) {
|
|
583
|
+
// Closed upstream — leave the open board.
|
|
584
|
+
await store.deleteWorkItem(id);
|
|
585
|
+
prunedCount++;
|
|
586
|
+
}
|
|
587
|
+
// else: still OPEN upstream (WIQL eventual-consistency lag) — KEEP
|
|
588
|
+
// it; the next full reconcile will handle it.
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (prunedCount > 0) {
|
|
593
|
+
events.emit('azdo.sync.pruned', { count: prunedCount });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
// Hierarchy self-heal (full mode only): provision Epics/Features, link
|
|
597
|
+
// unparented items, and re-parent mis-parented ones. Idempotent
|
|
598
|
+
// (find-or-create + KV cache) and self-limiting — once correct, the next
|
|
599
|
+
// pull shows the right parent so they land in `alreadyCorrect`. Failure
|
|
600
|
+
// here must NEVER crash the sync tick, hence its own try/catch.
|
|
601
|
+
if (effectiveMode === 'full' && hierItems.length > 0) {
|
|
602
|
+
try {
|
|
603
|
+
const projects = await resolveProjectConfigs(loadConfig(), store);
|
|
604
|
+
const hier = await provisionHierarchy({ client, store, organization: config.organization, project: config.project, projects }, hierItems);
|
|
605
|
+
if (hier.epicsCreated || hier.featuresCreated || hier.linked || hier.reparented) {
|
|
606
|
+
events.emit('azdo.sync.hierarchy', hier);
|
|
607
|
+
console.log(`[AzdoSync] Hierarchy: +${hier.epicsCreated} epics, +${hier.featuresCreated} features, ${hier.linked} linked, ${hier.reparented} reparented, ${hier.alreadyCorrect} ok, ${hier.skipped} skipped, ${hier.errors.length} errors`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
console.error('[AzdoSync] Hierarchy provisioning failed (sync continues):', err instanceof Error ? err.message : String(err));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
// Advance the watermark to (maxChangedDate − 60s) so the next incremental
|
|
615
|
+
// run re-scans a short overlap window (absorbs clock skew). Only advance
|
|
616
|
+
// forward — never rewind past a watermark a concurrent/prior run set.
|
|
617
|
+
if (maxChangedMs > 0) {
|
|
618
|
+
const nextWatermark = new Date(maxChangedMs - 60_000).toISOString();
|
|
619
|
+
const prevMs = currentWatermark ? new Date(currentWatermark).getTime() : 0;
|
|
620
|
+
if (!Number.isFinite(prevMs) || new Date(nextWatermark).getTime() > prevMs) {
|
|
621
|
+
await store.setSyncMeta(watermarkKey, nextWatermark);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
lastSuccessAt = Date.now();
|
|
625
|
+
lastError = undefined;
|
|
626
|
+
checkStale();
|
|
627
|
+
result.durationMs = Date.now() - t0;
|
|
628
|
+
events.emit('azdo.sync.tick', result);
|
|
629
|
+
return result;
|
|
630
|
+
}
|
|
631
|
+
catch (err) {
|
|
632
|
+
lastErrorAt = Date.now();
|
|
633
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
634
|
+
checkStale();
|
|
635
|
+
result.durationMs = Date.now() - t0;
|
|
636
|
+
throw err;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// ---------------------------------------------------------------------------
|
|
640
|
+
// pushLocalChange — push-up a single work item
|
|
641
|
+
// ---------------------------------------------------------------------------
|
|
642
|
+
async function pushLocalChange(workItemId) {
|
|
643
|
+
const local = await store.getWorkItem(workItemId);
|
|
644
|
+
if (!local) {
|
|
645
|
+
console.warn(`[AzdoSync] pushLocalChange: item ${workItemId} not found in local store`);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const numericId = parseInt(workItemId, 10);
|
|
649
|
+
if (isNaN(numericId)) {
|
|
650
|
+
console.warn(`[AzdoSync] pushLocalChange: ${workItemId} is not a numeric AzDO ID`);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
// Build patches from the LOCAL intent, not a fresh AzDO fetch. Re-fetching
|
|
654
|
+
// AzDO and re-deriving patches from its current state would overwrite the
|
|
655
|
+
// very local edit we're trying to push.
|
|
656
|
+
const patches = await buildLocalPushPatches(store, fieldNames, workItemId, local, config);
|
|
657
|
+
try {
|
|
658
|
+
await withExponentialBackoff(() => client.updateWorkItem(numericId, patches), 3, 1000, 30000);
|
|
659
|
+
}
|
|
660
|
+
catch (err) {
|
|
661
|
+
if (err instanceof AzdoApiError && err.status !== 429 && err.status !== 503) {
|
|
662
|
+
console.error(`[AzdoSync] Push failed for ${workItemId}:`, err.message);
|
|
663
|
+
events.emit('azdo.push.failed', { workItemId, status: err.status, message: err.message });
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
throw err;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
// ---------------------------------------------------------------------------
|
|
671
|
+
// start / stop
|
|
672
|
+
// ---------------------------------------------------------------------------
|
|
673
|
+
async function start() {
|
|
674
|
+
if (!config.enabled) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (started) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
started = true;
|
|
681
|
+
// Boot: a FULL sync seeds the complete backlog and establishes the
|
|
682
|
+
// watermark before the incremental heartbeat takes over.
|
|
683
|
+
inFlightSync = syncOnce({ mode: 'full' }).catch((err) => {
|
|
684
|
+
console.error('[AzdoSync] Initial sync failed:', err);
|
|
685
|
+
return { pulled: 0, pushed: 0, conflicts: 0, errors: [], durationMs: 0 };
|
|
686
|
+
});
|
|
687
|
+
await inFlightSync;
|
|
688
|
+
inFlightSync = null;
|
|
689
|
+
// Periodic FULL reconcile cadence: roughly every 15 minutes. A full pass
|
|
690
|
+
// re-scans the complete id set and prunes orphans (deletes that an
|
|
691
|
+
// incremental ChangedDate query can never observe). Every other tick is a
|
|
692
|
+
// cheap incremental pull.
|
|
693
|
+
const FULL_RECONCILE_MS = 900_000; // 15 minutes
|
|
694
|
+
const fullEveryNthTick = Math.max(1, Math.round(FULL_RECONCILE_MS / config.intervalMs));
|
|
695
|
+
let tick = 0;
|
|
696
|
+
intervalHandle = setInterval(() => {
|
|
697
|
+
tick += 1;
|
|
698
|
+
const mode = tick % fullEveryNthTick === 0 ? 'full' : 'incremental';
|
|
699
|
+
inFlightSync = syncOnce({ mode }).then(async () => {
|
|
700
|
+
if (drainAfterSync && root) {
|
|
701
|
+
const ctx = { client, store, config, events, fetchToken: deps.fetchToken };
|
|
702
|
+
try {
|
|
703
|
+
const drainResult = await drainWorkItemPushQueue(root, ctx);
|
|
704
|
+
console.log(`[AzdoSync] Drain complete: ${drainResult.drained} drained, ${drainResult.errors.length} errors`);
|
|
705
|
+
if (drainResult.errors.length > 0) {
|
|
706
|
+
console.warn('[AzdoSync] Drain errors:', drainResult.errors.map((e) => `${e.workItemId}(${e.lastError})`).join(', '));
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
catch (err) {
|
|
710
|
+
console.error('[AzdoSync] Drain threw unexpectedly (tick continues):', err instanceof Error ? err.message : String(err));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return { pulled: 0, pushed: 0, conflicts: 0, errors: [], durationMs: 0 };
|
|
714
|
+
}).catch((err) => {
|
|
715
|
+
console.error('[AzdoSync] Periodic sync failed:', err);
|
|
716
|
+
return { pulled: 0, pushed: 0, conflicts: 0, errors: [], durationMs: 0 };
|
|
717
|
+
}).finally(() => {
|
|
718
|
+
inFlightSync = null;
|
|
719
|
+
});
|
|
720
|
+
}, config.intervalMs);
|
|
721
|
+
}
|
|
722
|
+
async function stop() {
|
|
723
|
+
if (intervalHandle !== null) {
|
|
724
|
+
clearInterval(intervalHandle);
|
|
725
|
+
intervalHandle = null;
|
|
726
|
+
}
|
|
727
|
+
started = false;
|
|
728
|
+
// Wait for any in-flight sync to settle
|
|
729
|
+
if (inFlightSync) {
|
|
730
|
+
await inFlightSync.catch(() => undefined);
|
|
731
|
+
inFlightSync = null;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function getStatus() {
|
|
735
|
+
const now = Date.now();
|
|
736
|
+
const threshold = config.intervalMs * 3;
|
|
737
|
+
const isStale = lastSuccessAt === null || (now - lastSuccessAt) > threshold;
|
|
738
|
+
return {
|
|
739
|
+
lastSuccessAt,
|
|
740
|
+
lastErrorAt,
|
|
741
|
+
lastError,
|
|
742
|
+
running: started,
|
|
743
|
+
isStale,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
return { start, stop, syncOnce, pushLocalChange, getStatus };
|
|
747
|
+
}
|
|
748
|
+
//# sourceMappingURL=azureDevOpsSync.js.map
|