crewly 1.12.1 → 1.13.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/config/skills/_common/complete-body-shape.test.sh +113 -0
- package/config/skills/_common/no-dangling-symlinks.test.sh +63 -0
- package/config/skills/_common/task-pool-body-shape.test.sh +471 -0
- package/config/skills/agent/core/accept-task/execute.sh +30 -5
- package/config/skills/agent/core/block-task/execute.sh +59 -6
- package/config/skills/agent/core/complete-task/SKILL.md +37 -5
- package/config/skills/agent/core/complete-task/execute.sh +40 -9
- package/config/skills/agent/core/complete-task/execute.test.sh +213 -0
- package/config/skills/agent/core/create-task/SKILL.md +33 -0
- package/config/skills/agent/core/create-task/execute.sh +82 -23
- package/config/skills/agent/core/delegate-task/execute.sh +46 -0
- package/config/skills/agent/core/get-team-norms/SKILL.md +1 -1
- package/config/skills/agent/core/get-team-norms/execute.sh +73 -2
- package/config/skills/agent/core/handoff-task/execute.sh +8 -1
- package/config/skills/agent/core/report-status/SKILL.md +26 -0
- package/config/skills/agent/core/report-status/execute.sh +40 -5
- package/config/skills/orchestrator/delegate-task/SKILL.md +1 -1
- package/config/skills/orchestrator/delegate-task/execute.sh +45 -11
- package/config/skills/orchestrator/delegate-task/execute.test.sh +153 -1
- package/config/skills/team-leader/aggregate-results/SKILL.md +1 -1
- package/config/skills/team-leader/decompose-goal/execute.sh +11 -16
- package/config/skills/team-leader/delegate-task/SKILL.md +14 -1
- package/config/skills/team-leader/delegate-task/execute.sh +132 -43
- package/config/skills/team-leader/delegate-task/execute.test.sh +180 -27
- package/config/vitest-exclude.constants.ts +56 -0
- package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.controller.js +29 -7
- package/dist/backend/backend/src/controllers/chat/chat.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js +18 -3
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/slack/slack.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/slack/slack.controller.js +18 -15
- package/dist/backend/backend/src/controllers/slack/slack.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.d.ts +6 -1
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js +19 -10
- package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +20 -0
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/scripts/stamp-build.d.ts +25 -0
- package/dist/backend/backend/src/scripts/stamp-build.d.ts.map +1 -0
- package/dist/backend/backend/src/scripts/stamp-build.js +48 -0
- package/dist/backend/backend/src/scripts/stamp-build.js.map +1 -0
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +33 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts +78 -2
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +248 -44
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/types.d.ts +51 -3
- package/dist/backend/backend/src/services/agent/crewly-agent/types.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/types.js +31 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/types.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-client.service.d.ts +5 -0
- package/dist/backend/backend/src/services/cloud/cloud-client.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-client.service.js +7 -0
- package/dist/backend/backend/src/services/cloud/cloud-client.service.js.map +1 -1
- package/dist/backend/backend/src/services/event-bus/event-to-workitem-bridge.service.d.ts +15 -0
- package/dist/backend/backend/src/services/event-bus/event-to-workitem-bridge.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/event-bus/event-to-workitem-bridge.service.js +40 -0
- package/dist/backend/backend/src/services/event-bus/event-to-workitem-bridge.service.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/commitment-approval-guard.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/commitment-approval-guard.js +26 -0
- package/dist/backend/backend/src/services/orchestrator/commitment-approval-guard.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js +19 -3
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/orchestrator-status.service.d.ts +28 -3
- package/dist/backend/backend/src/services/orchestrator/orchestrator-status.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/orchestrator-status.service.js +65 -8
- package/dist/backend/backend/src/services/orchestrator/orchestrator-status.service.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts +214 -4
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.js +330 -48
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts +13 -1
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js +84 -5
- package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts +14 -0
- package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js +77 -3
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
- package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts +152 -7
- package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/task-pool/task-pool.service.js +268 -36
- package/dist/backend/backend/src/services/task-pool/task-pool.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/escalation-router.service.js +2 -2
- package/dist/backend/backend/src/services/v3/escalation-router.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/request-sla.subscriber.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/request-sla.subscriber.js +39 -2
- package/dist/backend/backend/src/services/v3/request-sla.subscriber.js.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-migrate.service.d.ts +3 -0
- package/dist/backend/backend/src/services/wiki/wiki-migrate.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/wiki/wiki-migrate.service.js +21 -10
- package/dist/backend/backend/src/services/wiki/wiki-migrate.service.js.map +1 -1
- package/dist/backend/backend/src/types/v2/index.d.ts +1 -1
- package/dist/backend/backend/src/types/v2/index.d.ts.map +1 -1
- package/dist/backend/backend/src/types/v2/index.js +1 -1
- package/dist/backend/backend/src/types/v2/index.js.map +1 -1
- package/dist/backend/backend/src/types/v2/work-item.types.d.ts +181 -1
- package/dist/backend/backend/src/types/v2/work-item.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/v2/work-item.types.js +104 -17
- package/dist/backend/backend/src/types/v2/work-item.types.js.map +1 -1
- package/dist/backend/backend/src/utils/build-provenance.d.ts +150 -0
- package/dist/backend/backend/src/utils/build-provenance.d.ts.map +1 -0
- package/dist/backend/backend/src/utils/build-provenance.js +211 -0
- package/dist/backend/backend/src/utils/build-provenance.js.map +1 -0
- package/dist/backend/backend/src/websocket/chat.gateway.d.ts.map +1 -1
- package/dist/backend/backend/src/websocket/chat.gateway.js +7 -1
- package/dist/backend/backend/src/websocket/chat.gateway.js.map +1 -1
- package/dist/backend/build-info.json +4 -0
- package/dist/backend/config/vitest-exclude.constants.d.ts +53 -0
- package/dist/backend/config/vitest-exclude.constants.d.ts.map +1 -0
- package/dist/backend/config/vitest-exclude.constants.js +55 -0
- package/dist/backend/config/vitest-exclude.constants.js.map +1 -0
- package/dist/cli/backend/src/services/agent/active-work-briefing.service.d.ts +538 -0
- package/dist/cli/backend/src/services/agent/active-work-briefing.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/active-work-briefing.service.js +802 -0
- package/dist/cli/backend/src/services/agent/active-work-briefing.service.js.map +1 -0
- package/dist/cli/backend/src/services/agent/agent-heartbeat.service.d.ts +240 -0
- package/dist/cli/backend/src/services/agent/agent-heartbeat.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/agent-heartbeat.service.js +524 -0
- package/dist/cli/backend/src/services/agent/agent-heartbeat.service.js.map +1 -0
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts +629 -0
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/agent-registration.service.js +4548 -0
- package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -0
- package/dist/cli/backend/src/services/agent/agent-suspend.service.d.ts +118 -0
- package/dist/cli/backend/src/services/agent/agent-suspend.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/agent-suspend.service.js +304 -0
- package/dist/cli/backend/src/services/agent/agent-suspend.service.js.map +1 -0
- package/dist/cli/backend/src/services/agent/auditor-scheduler.service.d.ts +196 -0
- package/dist/cli/backend/src/services/agent/auditor-scheduler.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/auditor-scheduler.service.js +504 -0
- package/dist/cli/backend/src/services/agent/auditor-scheduler.service.js.map +1 -0
- package/dist/cli/backend/src/services/agent/claude-runtime.service.d.ts +97 -0
- package/dist/cli/backend/src/services/agent/claude-runtime.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/claude-runtime.service.js +334 -0
- package/dist/cli/backend/src/services/agent/claude-runtime.service.js.map +1 -0
- package/dist/cli/backend/src/services/agent/codex-runtime.service.d.ts +47 -0
- package/dist/cli/backend/src/services/agent/codex-runtime.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/codex-runtime.service.js +112 -0
- package/dist/cli/backend/src/services/agent/codex-runtime.service.js.map +1 -0
- package/dist/cli/backend/src/services/agent/context-window-monitor.service.d.ts +323 -0
- package/dist/cli/backend/src/services/agent/context-window-monitor.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/context-window-monitor.service.js +1056 -0
- package/dist/cli/backend/src/services/agent/context-window-monitor.service.js.map +1 -0
- package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts +198 -0
- package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +816 -0
- package/dist/cli/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -0
- package/dist/cli/backend/src/services/agent/crewly-agent/in-process-log-buffer.d.ts +155 -0
- package/dist/cli/backend/src/services/agent/crewly-agent/in-process-log-buffer.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/crewly-agent/in-process-log-buffer.js +272 -0
- package/dist/cli/backend/src/services/agent/crewly-agent/in-process-log-buffer.js.map +1 -0
- package/dist/cli/backend/src/services/agent/crewly-agent/in-process-runtime-registry.d.ts +91 -0
- package/dist/cli/backend/src/services/agent/crewly-agent/in-process-runtime-registry.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/crewly-agent/in-process-runtime-registry.js +120 -0
- package/dist/cli/backend/src/services/agent/crewly-agent/in-process-runtime-registry.js.map +1 -0
- package/dist/cli/backend/src/services/agent/crewly-agent/types.d.ts +536 -0
- package/dist/cli/backend/src/services/agent/crewly-agent/types.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/crewly-agent/types.js +282 -0
- package/dist/cli/backend/src/services/agent/crewly-agent/types.js.map +1 -0
- package/dist/cli/backend/src/services/agent/disk-cleanup.service.d.ts +100 -0
- package/dist/cli/backend/src/services/agent/disk-cleanup.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/disk-cleanup.service.js +149 -0
- package/dist/cli/backend/src/services/agent/disk-cleanup.service.js.map +1 -0
- package/dist/cli/backend/src/services/agent/gemini-runtime.service.d.ts +130 -0
- package/dist/cli/backend/src/services/agent/gemini-runtime.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/gemini-runtime.service.js +758 -0
- package/dist/cli/backend/src/services/agent/gemini-runtime.service.js.map +1 -0
- package/dist/cli/backend/src/services/agent/oauth-relogin-monitor.service.d.ts +216 -0
- package/dist/cli/backend/src/services/agent/oauth-relogin-monitor.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/oauth-relogin-monitor.service.js +508 -0
- package/dist/cli/backend/src/services/agent/oauth-relogin-monitor.service.js.map +1 -0
- package/dist/cli/backend/src/services/agent/pty-activity-tracker.service.d.ts +113 -0
- package/dist/cli/backend/src/services/agent/pty-activity-tracker.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/pty-activity-tracker.service.js +161 -0
- package/dist/cli/backend/src/services/agent/pty-activity-tracker.service.js.map +1 -0
- package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.d.ts +156 -0
- package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js +641 -0
- package/dist/cli/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -0
- package/dist/cli/backend/src/services/agent/runtime-exit-monitor.service.d.ts +295 -0
- package/dist/cli/backend/src/services/agent/runtime-exit-monitor.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/runtime-exit-monitor.service.js +1166 -0
- package/dist/cli/backend/src/services/agent/runtime-exit-monitor.service.js.map +1 -0
- package/dist/cli/backend/src/services/agent/runtime-service.factory.d.ts +71 -0
- package/dist/cli/backend/src/services/agent/runtime-service.factory.d.ts.map +1 -0
- package/dist/cli/backend/src/services/agent/runtime-service.factory.js +174 -0
- package/dist/cli/backend/src/services/agent/runtime-service.factory.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-builder.service.d.ts +435 -0
- package/dist/cli/backend/src/services/ai/prompt-builder.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-builder.service.js +1331 -0
- package/dist/cli/backend/src/services/ai/prompt-builder.service.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/capability-overlay.module.d.ts +33 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/capability-overlay.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/capability-overlay.module.js +70 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/capability-overlay.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/communication.module.d.ts +100 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/communication.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/communication.module.js +296 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/communication.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/decision-rights.module.d.ts +59 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/decision-rights.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/decision-rights.module.js +87 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/decision-rights.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/default-execution-loop.module.d.ts +67 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/default-execution-loop.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/default-execution-loop.module.js +84 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/default-execution-loop.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/domain-sop.module.d.ts +35 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/domain-sop.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/domain-sop.module.js +50 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/domain-sop.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/expert-profile.module.d.ts +41 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/expert-profile.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/expert-profile.module.js +95 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/expert-profile.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/identity.module.d.ts +28 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/identity.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/identity.module.js +47 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/identity.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/lazy-anti-patterns.module.d.ts +65 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/lazy-anti-patterns.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/lazy-anti-patterns.module.js +79 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/lazy-anti-patterns.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/learning-reference.module.d.ts +28 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/learning-reference.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/learning-reference.module.js +55 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/learning-reference.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/lifecycle.module.d.ts +44 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/lifecycle.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/lifecycle.module.js +120 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/lifecycle.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/markdown-file-module.d.ts +46 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/markdown-file-module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/markdown-file-module.js +44 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/markdown-file-module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/memory-reference.module.d.ts +28 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/memory-reference.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/memory-reference.module.js +46 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/memory-reference.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/mission-context.module.d.ts +60 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/mission-context.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/mission-context.module.js +104 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/mission-context.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/project-reference.module.d.ts +29 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/project-reference.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/project-reference.module.js +50 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/project-reference.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-assembly.service.d.ts +103 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-assembly.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-assembly.service.js +383 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-assembly.service.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/recovery.module.d.ts +34 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/recovery.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/recovery.module.js +96 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/recovery.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/request-contract.module.d.ts +106 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/request-contract.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/request-contract.module.js +167 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/request-contract.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/risk-policy.module.d.ts +35 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/risk-policy.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/risk-policy.module.js +50 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/risk-policy.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/role-boundary.module.d.ts +91 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/role-boundary.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/role-boundary.module.js +411 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/role-boundary.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts +62 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +199 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/sop-norm-distinction.module.d.ts +79 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/sop-norm-distinction.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/sop-norm-distinction.module.js +118 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/sop-norm-distinction.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/soul.module.d.ts +102 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/soul.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/soul.module.js +229 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/soul.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/team-norms.module.d.ts +61 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/team-norms.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/team-norms.module.js +197 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/team-norms.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/team-reference.module.d.ts +54 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/team-reference.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/team-reference.module.js +149 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/team-reference.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/user-profile-reference.module.d.ts +32 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/user-profile-reference.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/user-profile-reference.module.js +69 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/user-profile-reference.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/working-memory.module.d.ts +91 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/working-memory.module.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/working-memory.module.js +136 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/working-memory.module.js.map +1 -0
- package/dist/cli/backend/src/services/ai/self-improvement/growth-areas.service.d.ts +81 -0
- package/dist/cli/backend/src/services/ai/self-improvement/growth-areas.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/self-improvement/growth-areas.service.js +135 -0
- package/dist/cli/backend/src/services/ai/self-improvement/growth-areas.service.js.map +1 -0
- package/dist/cli/backend/src/services/chat-v2/chat-v2.service.d.ts +737 -0
- package/dist/cli/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js +1213 -0
- package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js.map +1 -0
- package/dist/cli/backend/src/services/chat-v2/chat-v2.singleton.d.ts +27 -0
- package/dist/cli/backend/src/services/chat-v2/chat-v2.singleton.d.ts.map +1 -0
- package/dist/cli/backend/src/services/chat-v2/chat-v2.singleton.js +57 -0
- package/dist/cli/backend/src/services/chat-v2/chat-v2.singleton.js.map +1 -0
- package/dist/cli/backend/src/services/chat-v2/config.d.ts +100 -0
- package/dist/cli/backend/src/services/chat-v2/config.d.ts.map +1 -0
- package/dist/cli/backend/src/services/chat-v2/config.js +174 -0
- package/dist/cli/backend/src/services/chat-v2/config.js.map +1 -0
- package/dist/cli/backend/src/services/chat-v2/legacy-dto.utils.d.ts +118 -0
- package/dist/cli/backend/src/services/chat-v2/legacy-dto.utils.d.ts.map +1 -0
- package/dist/cli/backend/src/services/chat-v2/legacy-dto.utils.js +171 -0
- package/dist/cli/backend/src/services/chat-v2/legacy-dto.utils.js.map +1 -0
- package/dist/cli/backend/src/services/chat-v2/sqlite/channel.store.d.ts +188 -0
- package/dist/cli/backend/src/services/chat-v2/sqlite/channel.store.d.ts.map +1 -0
- package/dist/cli/backend/src/services/chat-v2/sqlite/channel.store.js +317 -0
- package/dist/cli/backend/src/services/chat-v2/sqlite/channel.store.js.map +1 -0
- package/dist/cli/backend/src/services/chat-v2/sqlite/chat-db.d.ts +119 -0
- package/dist/cli/backend/src/services/chat-v2/sqlite/chat-db.d.ts.map +1 -0
- package/dist/cli/backend/src/services/chat-v2/sqlite/chat-db.js +551 -0
- package/dist/cli/backend/src/services/chat-v2/sqlite/chat-db.js.map +1 -0
- package/dist/cli/backend/src/services/chat-v2/sqlite/message.store.d.ts +208 -0
- package/dist/cli/backend/src/services/chat-v2/sqlite/message.store.d.ts.map +1 -0
- package/dist/cli/backend/src/services/chat-v2/sqlite/message.store.js +405 -0
- package/dist/cli/backend/src/services/chat-v2/sqlite/message.store.js.map +1 -0
- package/dist/cli/backend/src/services/chat-v2/types.d.ts +341 -0
- package/dist/cli/backend/src/services/chat-v2/types.d.ts.map +1 -0
- package/dist/cli/backend/src/services/chat-v2/types.js +61 -0
- package/dist/cli/backend/src/services/chat-v2/types.js.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts +5 -0
- package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/cloud/cloud-client.service.js +7 -0
- package/dist/cli/backend/src/services/cloud/cloud-client.service.js.map +1 -1
- package/dist/cli/backend/src/services/continuation/patterns/idle-patterns.d.ts +37 -0
- package/dist/cli/backend/src/services/continuation/patterns/idle-patterns.d.ts.map +1 -0
- package/dist/cli/backend/src/services/continuation/patterns/idle-patterns.js +63 -0
- package/dist/cli/backend/src/services/continuation/patterns/idle-patterns.js.map +1 -0
- package/dist/cli/backend/src/services/intent-task/intent-classifier.rules.d.ts +335 -0
- package/dist/cli/backend/src/services/intent-task/intent-classifier.rules.d.ts.map +1 -0
- package/dist/cli/backend/src/services/intent-task/intent-classifier.rules.js +421 -0
- package/dist/cli/backend/src/services/intent-task/intent-classifier.rules.js.map +1 -0
- package/dist/cli/backend/src/services/mcp-client.d.ts +233 -0
- package/dist/cli/backend/src/services/mcp-client.d.ts.map +1 -0
- package/dist/cli/backend/src/services/mcp-client.js +302 -0
- package/dist/cli/backend/src/services/mcp-client.js.map +1 -0
- package/dist/cli/backend/src/services/memory/mission-context.service.d.ts +168 -0
- package/dist/cli/backend/src/services/memory/mission-context.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/memory/mission-context.service.js +365 -0
- package/dist/cli/backend/src/services/memory/mission-context.service.js.map +1 -0
- package/dist/cli/backend/src/services/memory/session-memory.service.d.ts +197 -0
- package/dist/cli/backend/src/services/memory/session-memory.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/memory/session-memory.service.js +369 -0
- package/dist/cli/backend/src/services/memory/session-memory.service.js.map +1 -0
- package/dist/cli/backend/src/services/memory/user-profile.service.d.ts +77 -0
- package/dist/cli/backend/src/services/memory/user-profile.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/memory/user-profile.service.js +171 -0
- package/dist/cli/backend/src/services/memory/user-profile.service.js.map +1 -0
- package/dist/cli/backend/src/services/memory/working-memory.service.d.ts +232 -0
- package/dist/cli/backend/src/services/memory/working-memory.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/memory/working-memory.service.js +417 -0
- package/dist/cli/backend/src/services/memory/working-memory.service.js.map +1 -0
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +84 -0
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js +136 -0
- package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -0
- package/dist/cli/backend/src/services/messaging/thread-status-queue.service.d.ts +358 -0
- package/dist/cli/backend/src/services/messaging/thread-status-queue.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/messaging/thread-status-queue.service.js +755 -0
- package/dist/cli/backend/src/services/messaging/thread-status-queue.service.js.map +1 -0
- package/dist/cli/backend/src/services/monitoring/token-usage.service.d.ts +269 -0
- package/dist/cli/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/monitoring/token-usage.service.js +391 -0
- package/dist/cli/backend/src/services/monitoring/token-usage.service.js.map +1 -0
- package/dist/cli/backend/src/services/observability/agent-behavior-log.service.d.ts +132 -0
- package/dist/cli/backend/src/services/observability/agent-behavior-log.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/observability/agent-behavior-log.service.js +284 -0
- package/dist/cli/backend/src/services/observability/agent-behavior-log.service.js.map +1 -0
- package/dist/cli/backend/src/services/observability/agent-behavior-log.singleton.d.ts +70 -0
- package/dist/cli/backend/src/services/observability/agent-behavior-log.singleton.d.ts.map +1 -0
- package/dist/cli/backend/src/services/observability/agent-behavior-log.singleton.js +121 -0
- package/dist/cli/backend/src/services/observability/agent-behavior-log.singleton.js.map +1 -0
- package/dist/cli/backend/src/services/observability/agent-behavior-log.types.d.ts +130 -0
- package/dist/cli/backend/src/services/observability/agent-behavior-log.types.d.ts.map +1 -0
- package/dist/cli/backend/src/services/observability/agent-behavior-log.types.js +48 -0
- package/dist/cli/backend/src/services/observability/agent-behavior-log.types.js.map +1 -0
- package/dist/cli/backend/src/services/observability/observability-db.d.ts +84 -0
- package/dist/cli/backend/src/services/observability/observability-db.d.ts.map +1 -0
- package/dist/cli/backend/src/services/observability/observability-db.js +165 -0
- package/dist/cli/backend/src/services/observability/observability-db.js.map +1 -0
- package/dist/cli/backend/src/services/onboarding/content-approval.service.d.ts +124 -0
- package/dist/cli/backend/src/services/onboarding/content-approval.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/onboarding/content-approval.service.js +256 -0
- package/dist/cli/backend/src/services/onboarding/content-approval.service.js.map +1 -0
- package/dist/cli/backend/src/services/onboarding/content-approval.types.d.ts +80 -0
- package/dist/cli/backend/src/services/onboarding/content-approval.types.d.ts.map +1 -0
- package/dist/cli/backend/src/services/onboarding/content-approval.types.js +16 -0
- package/dist/cli/backend/src/services/onboarding/content-approval.types.js.map +1 -0
- package/dist/cli/backend/src/services/orc/orc-delivery-enforcer.service.d.ts +142 -0
- package/dist/cli/backend/src/services/orc/orc-delivery-enforcer.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/orc/orc-delivery-enforcer.service.js +265 -0
- package/dist/cli/backend/src/services/orc/orc-delivery-enforcer.service.js.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/improvement-marker.service.d.ts +231 -0
- package/dist/cli/backend/src/services/orchestrator/improvement-marker.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/improvement-marker.service.js +353 -0
- package/dist/cli/backend/src/services/orchestrator/improvement-marker.service.js.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/improvement-startup.service.d.ts +157 -0
- package/dist/cli/backend/src/services/orchestrator/improvement-startup.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/improvement-startup.service.js +483 -0
- package/dist/cli/backend/src/services/orchestrator/improvement-startup.service.js.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/index.d.ts +16 -0
- package/dist/cli/backend/src/services/orchestrator/index.d.ts.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/index.js +16 -0
- package/dist/cli/backend/src/services/orchestrator/index.js.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/orchestrator-restart.service.d.ts +117 -0
- package/dist/cli/backend/src/services/orchestrator/orchestrator-restart.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/orchestrator-restart.service.js +300 -0
- package/dist/cli/backend/src/services/orchestrator/orchestrator-restart.service.js.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/orchestrator-setup.service.d.ts +114 -0
- package/dist/cli/backend/src/services/orchestrator/orchestrator-setup.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/orchestrator-setup.service.js +195 -0
- package/dist/cli/backend/src/services/orchestrator/orchestrator-setup.service.js.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/orchestrator-status.service.d.ts +94 -0
- package/dist/cli/backend/src/services/orchestrator/orchestrator-status.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/orchestrator-status.service.js +279 -0
- package/dist/cli/backend/src/services/orchestrator/orchestrator-status.service.js.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/safe-restart.service.d.ts +192 -0
- package/dist/cli/backend/src/services/orchestrator/safe-restart.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/safe-restart.service.js +395 -0
- package/dist/cli/backend/src/services/orchestrator/safe-restart.service.js.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/self-improvement.service.d.ts +201 -0
- package/dist/cli/backend/src/services/orchestrator/self-improvement.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/self-improvement.service.js +396 -0
- package/dist/cli/backend/src/services/orchestrator/self-improvement.service.js.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/slack-bridge-lazy.d.ts +17 -0
- package/dist/cli/backend/src/services/orchestrator/slack-bridge-lazy.d.ts.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/slack-bridge-lazy.js +30 -0
- package/dist/cli/backend/src/services/orchestrator/slack-bridge-lazy.js.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/state-persistence.service.d.ts +249 -0
- package/dist/cli/backend/src/services/orchestrator/state-persistence.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/orchestrator/state-persistence.service.js +569 -0
- package/dist/cli/backend/src/services/orchestrator/state-persistence.service.js.map +1 -0
- package/dist/cli/backend/src/services/session/index.d.ts +47 -0
- package/dist/cli/backend/src/services/session/index.d.ts.map +1 -0
- package/dist/cli/backend/src/services/session/index.js +50 -0
- package/dist/cli/backend/src/services/session/index.js.map +1 -0
- package/dist/cli/backend/src/services/session/log-rotation.service.d.ts +163 -0
- package/dist/cli/backend/src/services/session/log-rotation.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/session/log-rotation.service.js +362 -0
- package/dist/cli/backend/src/services/session/log-rotation.service.js.map +1 -0
- package/dist/cli/backend/src/services/session/pty/index.d.ts +11 -0
- package/dist/cli/backend/src/services/session/pty/index.d.ts.map +1 -0
- package/dist/cli/backend/src/services/session/pty/index.js +11 -0
- package/dist/cli/backend/src/services/session/pty/index.js.map +1 -0
- package/dist/cli/backend/src/services/session/pty/pty-session-backend.d.ts +445 -0
- package/dist/cli/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -0
- package/dist/cli/backend/src/services/session/pty/pty-session-backend.js +804 -0
- package/dist/cli/backend/src/services/session/pty/pty-session-backend.js.map +1 -0
- package/dist/cli/backend/src/services/session/pty/pty-session.d.ts +252 -0
- package/dist/cli/backend/src/services/session/pty/pty-session.d.ts.map +1 -0
- package/dist/cli/backend/src/services/session/pty/pty-session.js +611 -0
- package/dist/cli/backend/src/services/session/pty/pty-session.js.map +1 -0
- package/dist/cli/backend/src/services/session/pty/pty-terminal-buffer.d.ts +275 -0
- package/dist/cli/backend/src/services/session/pty/pty-terminal-buffer.d.ts.map +1 -0
- package/dist/cli/backend/src/services/session/pty/pty-terminal-buffer.js +403 -0
- package/dist/cli/backend/src/services/session/pty/pty-terminal-buffer.js.map +1 -0
- package/dist/cli/backend/src/services/session/runtime-pid-registry.service.d.ts +86 -0
- package/dist/cli/backend/src/services/session/runtime-pid-registry.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/session/runtime-pid-registry.service.js +214 -0
- package/dist/cli/backend/src/services/session/runtime-pid-registry.service.js.map +1 -0
- package/dist/cli/backend/src/services/session/session-backend.factory.d.ts +127 -0
- package/dist/cli/backend/src/services/session/session-backend.factory.d.ts.map +1 -0
- package/dist/cli/backend/src/services/session/session-backend.factory.js +226 -0
- package/dist/cli/backend/src/services/session/session-backend.factory.js.map +1 -0
- package/dist/cli/backend/src/services/session/session-backend.interface.d.ts +390 -0
- package/dist/cli/backend/src/services/session/session-backend.interface.d.ts.map +1 -0
- package/dist/cli/backend/src/services/session/session-backend.interface.js +19 -0
- package/dist/cli/backend/src/services/session/session-backend.interface.js.map +1 -0
- package/dist/cli/backend/src/services/session/session-command-helper.d.ts +350 -0
- package/dist/cli/backend/src/services/session/session-command-helper.d.ts.map +1 -0
- package/dist/cli/backend/src/services/session/session-command-helper.js +810 -0
- package/dist/cli/backend/src/services/session/session-command-helper.js.map +1 -0
- package/dist/cli/backend/src/services/session/session-handoff.service.d.ts +307 -0
- package/dist/cli/backend/src/services/session/session-handoff.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/session/session-handoff.service.js +742 -0
- package/dist/cli/backend/src/services/session/session-handoff.service.js.map +1 -0
- package/dist/cli/backend/src/services/session/session-state-persistence.d.ts +231 -0
- package/dist/cli/backend/src/services/session/session-state-persistence.d.ts.map +1 -0
- package/dist/cli/backend/src/services/session/session-state-persistence.js +407 -0
- package/dist/cli/backend/src/services/session/session-state-persistence.js.map +1 -0
- package/dist/cli/backend/src/services/settings/role.service.d.ts +277 -0
- package/dist/cli/backend/src/services/settings/role.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/settings/role.service.js +728 -0
- package/dist/cli/backend/src/services/settings/role.service.js.map +1 -0
- package/dist/cli/backend/src/services/slack/cross-machine-message.service.d.ts +152 -0
- package/dist/cli/backend/src/services/slack/cross-machine-message.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/slack/cross-machine-message.service.js +354 -0
- package/dist/cli/backend/src/services/slack/cross-machine-message.service.js.map +1 -0
- package/dist/cli/backend/src/services/slack/slack-image.service.d.ts +114 -0
- package/dist/cli/backend/src/services/slack/slack-image.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/slack/slack-image.service.js +331 -0
- package/dist/cli/backend/src/services/slack/slack-image.service.js.map +1 -0
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts +499 -0
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -0
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +1928 -0
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -0
- package/dist/cli/backend/src/services/slack/slack.service.d.ts +315 -0
- package/dist/cli/backend/src/services/slack/slack.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/slack/slack.service.js +1094 -0
- package/dist/cli/backend/src/services/slack/slack.service.js.map +1 -0
- package/dist/cli/backend/src/services/sop/sop.service.d.ts +364 -0
- package/dist/cli/backend/src/services/sop/sop.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/sop/sop.service.js +734 -0
- package/dist/cli/backend/src/services/sop/sop.service.js.map +1 -0
- package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts +152 -7
- package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/task-pool/task-pool.service.js +268 -36
- package/dist/cli/backend/src/services/task-pool/task-pool.service.js.map +1 -1
- package/dist/cli/backend/src/services/v3/escalation-router.service.d.ts +178 -0
- package/dist/cli/backend/src/services/v3/escalation-router.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/v3/escalation-router.service.js +692 -0
- package/dist/cli/backend/src/services/v3/escalation-router.service.js.map +1 -0
- package/dist/cli/backend/src/services/v3/kr-tracking.service.d.ts +200 -0
- package/dist/cli/backend/src/services/v3/kr-tracking.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/v3/kr-tracking.service.js +482 -0
- package/dist/cli/backend/src/services/v3/kr-tracking.service.js.map +1 -0
- package/dist/cli/backend/src/services/v3/mission-executor.service.d.ts +117 -0
- package/dist/cli/backend/src/services/v3/mission-executor.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/v3/mission-executor.service.js +321 -0
- package/dist/cli/backend/src/services/v3/mission-executor.service.js.map +1 -0
- package/dist/cli/backend/src/services/v3/request-sla.subscriber.d.ts +610 -0
- package/dist/cli/backend/src/services/v3/request-sla.subscriber.d.ts.map +1 -0
- package/dist/cli/backend/src/services/v3/request-sla.subscriber.js +1529 -0
- package/dist/cli/backend/src/services/v3/request-sla.subscriber.js.map +1 -0
- package/dist/cli/backend/src/services/v3/request-tracker.service.d.ts +78 -0
- package/dist/cli/backend/src/services/v3/request-tracker.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/v3/request-tracker.service.js +108 -0
- package/dist/cli/backend/src/services/v3/request-tracker.service.js.map +1 -0
- package/dist/cli/backend/src/services/v3/request.service.d.ts +295 -0
- package/dist/cli/backend/src/services/v3/request.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/v3/request.service.js +534 -0
- package/dist/cli/backend/src/services/v3/request.service.js.map +1 -0
- package/dist/cli/backend/src/services/v3/v3-data.service.d.ts +340 -0
- package/dist/cli/backend/src/services/v3/v3-data.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/v3/v3-data.service.js +1113 -0
- package/dist/cli/backend/src/services/v3/v3-data.service.js.map +1 -0
- package/dist/cli/backend/src/types/cross-machine.types.d.ts +103 -0
- package/dist/cli/backend/src/types/cross-machine.types.d.ts.map +1 -0
- package/dist/cli/backend/src/types/cross-machine.types.js +47 -0
- package/dist/cli/backend/src/types/cross-machine.types.js.map +1 -0
- package/dist/cli/backend/src/types/intent-task.types.d.ts +402 -0
- package/dist/cli/backend/src/types/intent-task.types.d.ts.map +1 -0
- package/dist/cli/backend/src/types/intent-task.types.js +403 -0
- package/dist/cli/backend/src/types/intent-task.types.js.map +1 -0
- package/dist/cli/backend/src/types/orchestrator-state.types.d.ts +531 -0
- package/dist/cli/backend/src/types/orchestrator-state.types.d.ts.map +1 -0
- package/dist/cli/backend/src/types/orchestrator-state.types.js +110 -0
- package/dist/cli/backend/src/types/orchestrator-state.types.js.map +1 -0
- package/dist/cli/backend/src/types/v2/index.d.ts +30 -0
- package/dist/cli/backend/src/types/v2/index.d.ts.map +1 -0
- package/dist/cli/backend/src/types/v2/index.js +22 -0
- package/dist/cli/backend/src/types/v2/index.js.map +1 -0
- package/dist/cli/backend/src/types/v2/key-result.types.d.ts +279 -0
- package/dist/cli/backend/src/types/v2/key-result.types.d.ts.map +1 -0
- package/dist/cli/backend/src/types/v2/key-result.types.js +264 -0
- package/dist/cli/backend/src/types/v2/key-result.types.js.map +1 -0
- package/dist/cli/backend/src/types/v2/mission.types.d.ts +661 -0
- package/dist/cli/backend/src/types/v2/mission.types.d.ts.map +1 -0
- package/dist/cli/backend/src/types/v2/mission.types.js +713 -0
- package/dist/cli/backend/src/types/v2/mission.types.js.map +1 -0
- package/dist/cli/backend/src/types/v2/reconcile.types.d.ts +204 -0
- package/dist/cli/backend/src/types/v2/reconcile.types.d.ts.map +1 -0
- package/dist/cli/backend/src/types/v2/reconcile.types.js +121 -0
- package/dist/cli/backend/src/types/v2/reconcile.types.js.map +1 -0
- package/dist/cli/backend/src/types/v2/request.types.d.ts +192 -0
- package/dist/cli/backend/src/types/v2/request.types.d.ts.map +1 -0
- package/dist/cli/backend/src/types/v2/request.types.js +179 -0
- package/dist/cli/backend/src/types/v2/request.types.js.map +1 -0
- package/dist/cli/backend/src/types/v2/work-item.types.d.ts +181 -1
- package/dist/cli/backend/src/types/v2/work-item.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/v2/work-item.types.js +104 -17
- package/dist/cli/backend/src/types/v2/work-item.types.js.map +1 -1
- package/dist/cli/backend/src/types/v2/workspace.types.d.ts +157 -0
- package/dist/cli/backend/src/types/v2/workspace.types.d.ts.map +1 -0
- package/dist/cli/backend/src/types/v2/workspace.types.js +173 -0
- package/dist/cli/backend/src/types/v2/workspace.types.js.map +1 -0
- package/dist/cli/backend/src/utils/async.utils.d.ts +68 -0
- package/dist/cli/backend/src/utils/async.utils.d.ts.map +1 -0
- package/dist/cli/backend/src/utils/async.utils.js +80 -0
- package/dist/cli/backend/src/utils/async.utils.js.map +1 -0
- package/dist/cli/backend/src/utils/native-binding.utils.d.ts +128 -0
- package/dist/cli/backend/src/utils/native-binding.utils.d.ts.map +1 -0
- package/dist/cli/backend/src/utils/native-binding.utils.js +206 -0
- package/dist/cli/backend/src/utils/native-binding.utils.js.map +1 -0
- package/dist/cli/backend/src/websocket/chat.gateway.d.ts +142 -0
- package/dist/cli/backend/src/websocket/chat.gateway.d.ts.map +1 -0
- package/dist/cli/backend/src/websocket/chat.gateway.js +327 -0
- package/dist/cli/backend/src/websocket/chat.gateway.js.map +1 -0
- package/dist/cli/backend/src/websocket/terminal.gateway.d.ts +341 -0
- package/dist/cli/backend/src/websocket/terminal.gateway.d.ts.map +1 -0
- package/dist/cli/backend/src/websocket/terminal.gateway.js +933 -0
- package/dist/cli/backend/src/websocket/terminal.gateway.js.map +1 -0
- package/dist/cli/config/vitest-exclude.constants.d.ts +53 -0
- package/dist/cli/config/vitest-exclude.constants.d.ts.map +1 -0
- package/dist/cli/config/vitest-exclude.constants.js +55 -0
- package/dist/cli/config/vitest-exclude.constants.js.map +1 -0
- package/package.json +2 -2
- package/packages/crewly-agent/src/cli.ts +16 -6
- package/packages/crewly-agent/src/runtime/agent-runner.service.test.ts +311 -33
- package/packages/crewly-agent/src/runtime/agent-runner.service.ts +221 -12
- package/packages/crewly-agent/src/runtime/tool-registry.test.ts +19 -0
- package/packages/crewly-agent/src/runtime/tool-registry.ts +4 -1
- package/packages/crewly-agent/src/runtime/types.ts +65 -5
|
@@ -0,0 +1,1928 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack-Orchestrator Bridge
|
|
3
|
+
*
|
|
4
|
+
* Routes messages between Slack and the Crewly orchestrator,
|
|
5
|
+
* enabling mobile control of AI teams.
|
|
6
|
+
*
|
|
7
|
+
* @module services/slack/bridge
|
|
8
|
+
*/
|
|
9
|
+
import { EventEmitter } from 'events';
|
|
10
|
+
import { promises as fs, createWriteStream } from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import os from 'os';
|
|
13
|
+
import { Readable } from 'stream';
|
|
14
|
+
import { pipeline } from 'stream/promises';
|
|
15
|
+
import { PDFParse } from 'pdf-parse';
|
|
16
|
+
import { getSlackService } from './slack.service.js';
|
|
17
|
+
import { getChatV2Service } from '../chat-v2/chat-v2.singleton.js';
|
|
18
|
+
import { synthesizeSlackConversationId } from '../chat-v2/legacy-dto.utils.js';
|
|
19
|
+
import { isOrchestratorActive, isAgentActive, getOrchestratorOfflineMessage, triggerOrchestratorSetup, } from '../orchestrator/index.js';
|
|
20
|
+
import { parseCommandIntent, } from '../../types/slack.types.js';
|
|
21
|
+
import { ContentApprovalService } from '../onboarding/content-approval.service.js';
|
|
22
|
+
import { getSlackImageService } from './slack-image.service.js';
|
|
23
|
+
import { ORCHESTRATOR_SESSION_NAME, MESSAGE_QUEUE_CONSTANTS, SLACK_IMAGE_CONSTANTS, SLACK_FILE_DOWNLOAD_CONSTANTS, SLACK_BRIDGE_CONSTANTS, AUDITOR_SCHEDULER_CONSTANTS, THREAD_STATUS_CONSTANTS } from '../../constants.js';
|
|
24
|
+
import { LoggerService } from '../core/logger.service.js';
|
|
25
|
+
import { CROSS_MACHINE_PREFIX } from '../../types/cross-machine.types.js';
|
|
26
|
+
import { getCrossMachineMessageService } from './cross-machine-message.service.js';
|
|
27
|
+
import { TERMINAL_REQUEST_STATUSES } from '../../types/v2/request.types.js';
|
|
28
|
+
/**
|
|
29
|
+
* Default bridge configuration
|
|
30
|
+
*/
|
|
31
|
+
const DEFAULT_CONFIG = {
|
|
32
|
+
orchestratorSession: ORCHESTRATOR_SESSION_NAME,
|
|
33
|
+
showTypingIndicator: true,
|
|
34
|
+
maxResponseLength: 3000,
|
|
35
|
+
enableNotifications: true,
|
|
36
|
+
responseTimeoutMs: (MESSAGE_QUEUE_CONSTANTS?.DEFAULT_MESSAGE_TIMEOUT ?? 120000) + 5000,
|
|
37
|
+
skillDeliveryWaitMs: SLACK_BRIDGE_CONSTANTS?.SKILL_DELIVERY_WAIT_MS ?? 3000,
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Maximum time to wait for an auto-recovery setup attempt before giving
|
|
41
|
+
* up and falling through to the offline path. Capped tight to avoid
|
|
42
|
+
* blocking the message ingress thread when setup is wedged.
|
|
43
|
+
*/
|
|
44
|
+
const AUTO_RECOVERY_TIMEOUT_MS = 5_000;
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Auto-Request suppression gates (Pipeline-#4 fix, Patch D)
|
|
47
|
+
//
|
|
48
|
+
// Spec: 2026-05-05-request-decompose-pipeline-gap.md §4 Patch D.
|
|
49
|
+
// Live data measured ~80% of auto-Requests in D0AC7NF5N7L matched one of
|
|
50
|
+
// these gates and were never actionable.
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
/**
|
|
53
|
+
* Minimum trimmed text length required to create an auto-Request. Anything
|
|
54
|
+
* shorter is treated as "trivial" (length-gate). Set deliberately tight so a
|
|
55
|
+
* one-sentence command like "fix the build" (15 chars) still passes.
|
|
56
|
+
*/
|
|
57
|
+
const MIN_AUTO_REQUEST_TEXT_LENGTH = 12;
|
|
58
|
+
/**
|
|
59
|
+
* Trimmed-text regex for trivial acknowledgement messages — anchored,
|
|
60
|
+
* case-insensitive, allows trailing punctuation/emoji whitespace. Matches
|
|
61
|
+
* "ok", "好的", "thx", "👍", etc. Add new locale tokens here as needed.
|
|
62
|
+
*/
|
|
63
|
+
const TRIVIAL_ACK_PATTERN = /^(ok|好的|收到|thx|thanks|谢谢|👍|✅|got it|sure|yes|是|对|对的)\W*$/i;
|
|
64
|
+
/**
|
|
65
|
+
* Trimmed-text regex matching the synthetic `[Slack File: …]` titles
|
|
66
|
+
* generated by the file-handling path when a user uploads a file with no
|
|
67
|
+
* narrative. Caught before the thread-continuation gate to avoid the lookup.
|
|
68
|
+
*/
|
|
69
|
+
const FILE_ONLY_TITLE_PATTERN = /^\[Slack File:[^\]]*\]\.?\s*$/;
|
|
70
|
+
/**
|
|
71
|
+
* Pure pre-fetch gates for auto-Request suppression (Patch D). Exported for
|
|
72
|
+
* focused unit testing — the full thread-continuation gate also lives in
|
|
73
|
+
* {@link SlackOrchestratorBridge.shouldSuppressAutoRequest} but requires a
|
|
74
|
+
* RequestService lookup so cannot be a pure function.
|
|
75
|
+
*
|
|
76
|
+
* Each function returns the suppression reason string (matches the values
|
|
77
|
+
* logged at debug level) or `null` when the gate does not match.
|
|
78
|
+
*/
|
|
79
|
+
export function suppressTrivialOrShort(rawText) {
|
|
80
|
+
const trimmed = rawText.trim();
|
|
81
|
+
if (trimmed.length < MIN_AUTO_REQUEST_TEXT_LENGTH || TRIVIAL_ACK_PATTERN.test(trimmed)) {
|
|
82
|
+
return 'trivial_or_short';
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
export function suppressFileOnly(rawText) {
|
|
87
|
+
const trimmed = rawText.trim();
|
|
88
|
+
return FILE_ONLY_TITLE_PATTERN.test(trimmed) ? 'file_only' : null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Slack-Orchestrator Bridge singleton
|
|
92
|
+
*/
|
|
93
|
+
let bridgeInstance = null;
|
|
94
|
+
/**
|
|
95
|
+
* Derive a short, readable chat-v2 channel name from a Slack thread's first
|
|
96
|
+
* message — so the consolidated chat shows a meaningful title rather than the
|
|
97
|
+
* synthesized `slack-<chan>-<ts>` id (which is identical across a channel's
|
|
98
|
+
* threads). Collapses whitespace and truncates; falls back to "Slack thread".
|
|
99
|
+
*
|
|
100
|
+
* @param content - The first inbound message of the thread.
|
|
101
|
+
* @returns A title like `Slack: ship the v2 launch deck`.
|
|
102
|
+
*/
|
|
103
|
+
export function deriveSlackThreadName(content) {
|
|
104
|
+
const oneLine = (content ?? '').replace(/\s+/g, ' ').trim();
|
|
105
|
+
if (!oneLine)
|
|
106
|
+
return 'Slack thread';
|
|
107
|
+
const snippet = oneLine.length > 48 ? `${oneLine.slice(0, 48)}…` : oneLine;
|
|
108
|
+
return `Slack: ${snippet}`;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* SlackOrchestratorBridge class
|
|
112
|
+
*
|
|
113
|
+
* Routes messages between Slack and the Crewly orchestrator.
|
|
114
|
+
* Handles command parsing, response formatting, and proactive notifications.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```typescript
|
|
118
|
+
* const bridge = getSlackOrchestratorBridge();
|
|
119
|
+
* await bridge.initialize();
|
|
120
|
+
*
|
|
121
|
+
* // Send notification when task completes
|
|
122
|
+
* await bridge.notifyTaskCompleted('Fix bug', 'Developer', 'MyProject');
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export class SlackOrchestratorBridge extends EventEmitter {
|
|
126
|
+
logger = LoggerService.getInstance().createComponentLogger('SlackBridge');
|
|
127
|
+
slackService;
|
|
128
|
+
chatV2;
|
|
129
|
+
messageQueueService = null;
|
|
130
|
+
threadStatusQueue = null;
|
|
131
|
+
threadStore = null;
|
|
132
|
+
config;
|
|
133
|
+
initialized = false;
|
|
134
|
+
/** Track if we've already logged the missing scope warning */
|
|
135
|
+
loggedMissingScope = false;
|
|
136
|
+
/**
|
|
137
|
+
* Pending completion reactions keyed by "channelId:threadTs".
|
|
138
|
+
* Stores the original message ts so that the ✅ reaction is added to the
|
|
139
|
+
* correct message when the reply-slack skill delivers a response.
|
|
140
|
+
*/
|
|
141
|
+
pendingReactions = new Map();
|
|
142
|
+
/**
|
|
143
|
+
* Track channel+thread pairs where Slack delivery was already handled
|
|
144
|
+
* by the reply-slack skill (execute.sh). Used by sendSlackResponse to
|
|
145
|
+
* avoid sending a duplicate fallback message to the same thread.
|
|
146
|
+
*/
|
|
147
|
+
/**
|
|
148
|
+
* Create a new SlackOrchestratorBridge
|
|
149
|
+
*
|
|
150
|
+
* @param config - Partial configuration to override defaults
|
|
151
|
+
*/
|
|
152
|
+
constructor(config = {}) {
|
|
153
|
+
super();
|
|
154
|
+
this.slackService = getSlackService();
|
|
155
|
+
this.chatV2 = getChatV2Service();
|
|
156
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Initialize the bridge
|
|
160
|
+
*
|
|
161
|
+
* Sets up message listeners and orchestrator subscriptions.
|
|
162
|
+
*
|
|
163
|
+
* @returns Promise that resolves when initialized
|
|
164
|
+
*/
|
|
165
|
+
async initialize() {
|
|
166
|
+
if (this.initialized)
|
|
167
|
+
return;
|
|
168
|
+
// Listen for Slack messages
|
|
169
|
+
this.slackService.on('message', this.handleSlackMessage.bind(this));
|
|
170
|
+
// Initialize cross-machine messaging and route received messages to orchestrator
|
|
171
|
+
try {
|
|
172
|
+
const crossMachineService = getCrossMachineMessageService();
|
|
173
|
+
await crossMachineService.initialize();
|
|
174
|
+
crossMachineService.on('message', (msg) => {
|
|
175
|
+
this.handleCrossMachineMessage(msg);
|
|
176
|
+
});
|
|
177
|
+
this.logger.info('Cross-machine message listener registered');
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
this.logger.warn('Cross-machine messaging not available', {
|
|
181
|
+
error: err instanceof Error ? err.message : String(err),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
this.initialized = true;
|
|
185
|
+
this.logger.info('Initialized');
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Check if bridge is initialized
|
|
189
|
+
*
|
|
190
|
+
* @returns True if initialized
|
|
191
|
+
*/
|
|
192
|
+
isInitialized() {
|
|
193
|
+
return this.initialized;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Set the message queue service for enqueuing messages to the orchestrator.
|
|
197
|
+
* Called during server initialization.
|
|
198
|
+
*
|
|
199
|
+
* @param service - The MessageQueueService instance
|
|
200
|
+
*/
|
|
201
|
+
setMessageQueueService(service) {
|
|
202
|
+
this.messageQueueService = service;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Set the thread status queue service for tracking inbound message lifecycle.
|
|
206
|
+
* Called during server initialization.
|
|
207
|
+
*
|
|
208
|
+
* @param service - The ThreadStatusQueueService instance
|
|
209
|
+
*/
|
|
210
|
+
setThreadStatusQueue(service) {
|
|
211
|
+
this.threadStatusQueue = service;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Set the Slack thread store service for persisting thread conversations.
|
|
215
|
+
* Called during server initialization.
|
|
216
|
+
*
|
|
217
|
+
* @param store - The SlackThreadStoreService instance
|
|
218
|
+
*/
|
|
219
|
+
setSlackThreadStore(store) {
|
|
220
|
+
this.threadStore = store;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Get current configuration
|
|
224
|
+
*
|
|
225
|
+
* @returns A copy of the current configuration
|
|
226
|
+
*/
|
|
227
|
+
getConfig() {
|
|
228
|
+
return { ...this.config };
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Handle incoming Slack message
|
|
232
|
+
*
|
|
233
|
+
* Parses the command, routes to appropriate handler, and sends response.
|
|
234
|
+
*
|
|
235
|
+
* @param message - Incoming Slack message
|
|
236
|
+
*/
|
|
237
|
+
async handleSlackMessage(message) {
|
|
238
|
+
this.logger.info('Received message', {
|
|
239
|
+
preview: (message.text || '').substring(0, 50),
|
|
240
|
+
hasImages: message.hasImages,
|
|
241
|
+
hasFiles: message.hasFiles,
|
|
242
|
+
fileCount: message.files?.length || 0,
|
|
243
|
+
});
|
|
244
|
+
try {
|
|
245
|
+
// Cross-machine message intercept — detect and route before normal processing
|
|
246
|
+
if (message.text && message.text.startsWith(CROSS_MACHINE_PREFIX)) {
|
|
247
|
+
const crossMachineService = getCrossMachineMessageService();
|
|
248
|
+
if (crossMachineService.isEnabled()) {
|
|
249
|
+
const handled = crossMachineService.handleIncomingMessage(message);
|
|
250
|
+
if (handled) {
|
|
251
|
+
this.logger.debug('Cross-machine message handled', {
|
|
252
|
+
preview: message.text.substring(0, 80),
|
|
253
|
+
});
|
|
254
|
+
return; // Don't process as a normal message
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Download files if present (both images and non-image files)
|
|
259
|
+
if (message.hasFiles && message.files) {
|
|
260
|
+
const imageFiles = message.files.filter(f => f.mimetype?.startsWith('image/'));
|
|
261
|
+
const nonImageFiles = message.files.filter(f => !f.mimetype?.startsWith('image/'));
|
|
262
|
+
// Download images via existing path (pass only imageFiles to avoid redundant API calls)
|
|
263
|
+
if (imageFiles.length > 0) {
|
|
264
|
+
await this.downloadMessageImages(message, imageFiles);
|
|
265
|
+
}
|
|
266
|
+
// Download non-image files
|
|
267
|
+
if (nonImageFiles.length > 0) {
|
|
268
|
+
await this.downloadMessageFiles(message, nonImageFiles);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Get or create conversation context
|
|
272
|
+
const context = this.slackService.getConversationContext(message.threadTs || message.ts, message.channelId, message.userId);
|
|
273
|
+
// Build enriched text with file references for the agent
|
|
274
|
+
const enrichedText = this.enrichTextWithFiles(message);
|
|
275
|
+
// Store inbound message in thread file (with image metadata)
|
|
276
|
+
if (this.threadStore) {
|
|
277
|
+
const threadTs = message.threadTs || message.ts;
|
|
278
|
+
try {
|
|
279
|
+
const userName = message.user?.realName || message.user?.name || message.userId;
|
|
280
|
+
await this.threadStore.appendUserMessage(message.channelId, threadTs, userName, enrichedText, message.images);
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
this.logger.warn('Failed to store thread message', { error: err instanceof Error ? err.message : String(err) });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Override message text with enriched version for downstream processing
|
|
287
|
+
message.text = enrichedText;
|
|
288
|
+
// Auditor prefix routing — intercept "auditor ...", "/auditor ...", or "@auditor ..." messages
|
|
289
|
+
const auditorMatch = enrichedText.match(/^[/@]?auditor[\s:]+(.+)/is);
|
|
290
|
+
if (auditorMatch) {
|
|
291
|
+
const auditorMessage = auditorMatch[1].trim();
|
|
292
|
+
try {
|
|
293
|
+
if (this.config.showTypingIndicator) {
|
|
294
|
+
await this.addTypingIndicator(message);
|
|
295
|
+
}
|
|
296
|
+
const { AuditorSchedulerService } = await import('../agent/auditor-scheduler.service.js');
|
|
297
|
+
const scheduler = AuditorSchedulerService.getInstance();
|
|
298
|
+
await scheduler.handleUserMessage(auditorMessage, {
|
|
299
|
+
channelId: message.channelId,
|
|
300
|
+
threadTs: message.threadTs || message.ts,
|
|
301
|
+
});
|
|
302
|
+
if (this.config.showTypingIndicator) {
|
|
303
|
+
await this.markComplete(message);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
await this.sendErrorResponse(message, error);
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
// #177: @mention routing — route to specific agent if @name is detected
|
|
312
|
+
const mentionTarget = await this.resolveMentionTarget(enrichedText);
|
|
313
|
+
if (mentionTarget) {
|
|
314
|
+
const isActive = await isAgentActive(mentionTarget.sessionName);
|
|
315
|
+
if (isActive) {
|
|
316
|
+
this.logger.info('Routing message to mentioned agent', {
|
|
317
|
+
agent: mentionTarget.name,
|
|
318
|
+
session: mentionTarget.sessionName,
|
|
319
|
+
});
|
|
320
|
+
if (this.config.showTypingIndicator) {
|
|
321
|
+
await this.addTypingIndicator(message);
|
|
322
|
+
}
|
|
323
|
+
// Strip the @mention prefix and send to the target agent
|
|
324
|
+
const cleanMessage = enrichedText.replace(new RegExp(`^@${mentionTarget.name}\\s*`, 'i'), '').trim();
|
|
325
|
+
const agentResponse = await this.sendToAgent(mentionTarget.sessionName, cleanMessage || enrichedText, context);
|
|
326
|
+
// Issue #394: `fromOrcReply` comes from the envelope — only
|
|
327
|
+
// `true` when slackResolve fired with a non-empty body. The
|
|
328
|
+
// pre-fix `response.length > 0` proxy false-positived on the
|
|
329
|
+
// 3 placeholder strings (timeout, direct-delivery, error),
|
|
330
|
+
// any of which would have auto-resolved the SLA tracker on a
|
|
331
|
+
// non-reply.
|
|
332
|
+
await this.sendSlackResponse(message, agentResponse.response, agentResponse.fromOrcReply);
|
|
333
|
+
if (this.config.showTypingIndicator) {
|
|
334
|
+
await this.markComplete(message);
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
this.logger.info('Mentioned agent is inactive, falling back to orchestrator', {
|
|
340
|
+
agent: mentionTarget.name,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// Parse command intent
|
|
345
|
+
const command = this.parseCommand(message.text);
|
|
346
|
+
// Add typing indicator
|
|
347
|
+
if (this.config.showTypingIndicator) {
|
|
348
|
+
await this.addTypingIndicator(message);
|
|
349
|
+
}
|
|
350
|
+
// Handle based on intent.
|
|
351
|
+
// `response` is the user-visible text; `fromOrcReply` tracks whether
|
|
352
|
+
// the response came from the orc's reply-slack callback (true) or a
|
|
353
|
+
// local string / fallback / placeholder (false). Only true responses
|
|
354
|
+
// may auto-resolve the V3 SLA tracker downstream.
|
|
355
|
+
let response;
|
|
356
|
+
let fromOrcReply = false;
|
|
357
|
+
let isOrchestratorRoute = false;
|
|
358
|
+
switch (command.intent) {
|
|
359
|
+
case 'help':
|
|
360
|
+
response = this.getHelpMessage();
|
|
361
|
+
break;
|
|
362
|
+
case 'status': {
|
|
363
|
+
const r = await this.handleStatusCommand(command, context);
|
|
364
|
+
response = r.response;
|
|
365
|
+
fromOrcReply = r.fromOrcReply;
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
case 'list_projects':
|
|
369
|
+
case 'list_teams':
|
|
370
|
+
case 'list_agents': {
|
|
371
|
+
const r = await this.handleListCommand(command, context);
|
|
372
|
+
response = r.response;
|
|
373
|
+
fromOrcReply = r.fromOrcReply;
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
case 'pause':
|
|
377
|
+
case 'resume': {
|
|
378
|
+
const r = await this.handleControlCommand(command, context);
|
|
379
|
+
response = r.response;
|
|
380
|
+
fromOrcReply = r.fromOrcReply;
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
default:
|
|
384
|
+
// V3 Request creation for Slack user messages — fire-and-forget
|
|
385
|
+
setImmediate(async () => {
|
|
386
|
+
try {
|
|
387
|
+
const { RequestService } = await import('../v3/request.service.js');
|
|
388
|
+
const svc = RequestService.getInstance();
|
|
389
|
+
// 2026-05-13 dogfood fix: encode BOTH thread root AND message
|
|
390
|
+
// ts in sourceConversationItemId. The previous form was
|
|
391
|
+
// `slack-{channel}-{message.ts}`, which made
|
|
392
|
+
// `extractSlackThreadTs(sourceConv)` return the per-message
|
|
393
|
+
// ts — never the actual Slack thread root. The SLA
|
|
394
|
+
// threadIndex therefore mapped per-message ts → requestId,
|
|
395
|
+
// but when orc replied via reply-slack → POST /api/slack/send
|
|
396
|
+
// → markResolvedByThread(realThreadRoot), the lookup missed
|
|
397
|
+
// and the Request never auto-closed (PR #538 hook was a
|
|
398
|
+
// no-op for thread-reply messages).
|
|
399
|
+
//
|
|
400
|
+
// New form:
|
|
401
|
+
// Top-level message (msg.ts == thread root):
|
|
402
|
+
// `slack-{channelId}-{ts}` (unchanged)
|
|
403
|
+
// Reply within an existing thread:
|
|
404
|
+
// `slack-{channelId}-{threadRoot}-msg-{messageTs}`
|
|
405
|
+
//
|
|
406
|
+
// The `-msg-{...}` suffix preserves per-message uniqueness
|
|
407
|
+
// so `findBySourceConversationItemId` still works. The
|
|
408
|
+
// updated `parseSlackThreadContext` strips the suffix
|
|
409
|
+
// before extracting the threadTs, so the SLA's threadIndex
|
|
410
|
+
// now keys on the actual thread root for both shapes.
|
|
411
|
+
const threadRoot = message.threadTs || message.ts;
|
|
412
|
+
const msgId = message.threadTs && message.threadTs !== message.ts
|
|
413
|
+
? `slack-${message.channelId}-${threadRoot}-msg-${message.ts}`
|
|
414
|
+
: `slack-${message.channelId}-${message.ts}`;
|
|
415
|
+
const existing = await svc.findBySourceConversationItemId(msgId);
|
|
416
|
+
if (existing)
|
|
417
|
+
return;
|
|
418
|
+
const rawText = message.text || '';
|
|
419
|
+
// Pipeline-#4 fix (spec 2026-05-05-request-decompose-pipeline-gap.md,
|
|
420
|
+
// Patch D): suppress trivial / continuation / file-only Slack messages
|
|
421
|
+
// that pollute the Request stream without ever becoming actionable
|
|
422
|
+
// work. ~80% of historical auto-Requests in D0AC7NF5N7L matched one
|
|
423
|
+
// of these gates (dogfood data 2026-05-05).
|
|
424
|
+
const suppressionReason = await this.shouldSuppressAutoRequest(message, rawText, svc);
|
|
425
|
+
if (suppressionReason) {
|
|
426
|
+
this.logger.debug('V3 Request creation suppressed', { msgId, reason: suppressionReason });
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const { generateRequestTitle, classifyIntent } = await import('../v3/v3-data.service.js');
|
|
430
|
+
// Auto-classify so request-decompose.subscriber sees correct
|
|
431
|
+
// intentLevel/intentCategory. Without this, every Slack-auto
|
|
432
|
+
// Request defaulted to L1+other and was never decomposed
|
|
433
|
+
// into delegation WorkItems — the entire Slack→WorkItem
|
|
434
|
+
// pipeline was silently no-op.
|
|
435
|
+
const { intentLevel, intentCategory } = classifyIntent(rawText);
|
|
436
|
+
const request = await svc.create({
|
|
437
|
+
title: generateRequestTitle(rawText, intentCategory),
|
|
438
|
+
description: rawText,
|
|
439
|
+
sourceConversationItemId: msgId,
|
|
440
|
+
priority: 'normal',
|
|
441
|
+
tags: ['slack'],
|
|
442
|
+
intentLevel,
|
|
443
|
+
intentCategory,
|
|
444
|
+
});
|
|
445
|
+
// P2-2: RequestTracker.setActiveRequest write removed. The
|
|
446
|
+
// companion read in v3-data.service.ts no longer falls back
|
|
447
|
+
// to time-window correlation; downstream delegate-task
|
|
448
|
+
// skills must now pass requestId explicitly.
|
|
449
|
+
this.logger.debug('V3 Request created from Slack message', { msgId, requestId: request.id });
|
|
450
|
+
}
|
|
451
|
+
catch (err) {
|
|
452
|
+
this.logger.warn('V3 Request creation from Slack failed (non-critical)', {
|
|
453
|
+
error: err instanceof Error ? err.message : String(err),
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
// Send to orchestrator for processing
|
|
458
|
+
{
|
|
459
|
+
const r = await this.sendToOrchestrator(message.text, context);
|
|
460
|
+
response = r.response;
|
|
461
|
+
fromOrcReply = r.fromOrcReply;
|
|
462
|
+
}
|
|
463
|
+
isOrchestratorRoute = true;
|
|
464
|
+
}
|
|
465
|
+
// Send response back to Slack. The `fromOrcReply` flag gates the V3
|
|
466
|
+
// SLA auto-resolve hook inside sendSlackResponse — placeholders and
|
|
467
|
+
// fallbacks must NOT mark the respond_to_user WI as resolved.
|
|
468
|
+
await this.sendSlackResponse(message, response, fromOrcReply);
|
|
469
|
+
// For orchestrator-routed messages, defer ✅ until reply-slack delivers
|
|
470
|
+
// the actual response. Store pending reaction keyed by channel+thread
|
|
471
|
+
// so addCompletionReaction() can find it when the reply arrives.
|
|
472
|
+
if (isOrchestratorRoute && this.config.showTypingIndicator) {
|
|
473
|
+
const threadTs = message.threadTs || message.ts;
|
|
474
|
+
const key = `${message.channelId}:${threadTs}`;
|
|
475
|
+
this.pendingReactions.set(key, message.ts);
|
|
476
|
+
}
|
|
477
|
+
else if (this.config.showTypingIndicator) {
|
|
478
|
+
// Non-orchestrator commands complete immediately
|
|
479
|
+
await this.markComplete(message);
|
|
480
|
+
}
|
|
481
|
+
this.emit('message_handled', { message, response });
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
this.logger.error('Error handling message', { error: error instanceof Error ? error.message : String(error) });
|
|
485
|
+
await this.sendErrorResponse(message, error);
|
|
486
|
+
this.emit('error', error);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Handle an incoming cross-machine message.
|
|
491
|
+
* Routes the message payload to the local orchestrator via the message queue.
|
|
492
|
+
*
|
|
493
|
+
* @param msg - Parsed cross-machine message
|
|
494
|
+
*/
|
|
495
|
+
async handleCrossMachineMessage(msg) {
|
|
496
|
+
this.logger.info('Routing cross-machine message to orchestrator', {
|
|
497
|
+
from: msg.fromName,
|
|
498
|
+
type: msg.type,
|
|
499
|
+
messageId: msg.messageId,
|
|
500
|
+
});
|
|
501
|
+
// Format as a human-readable message for the orchestrator
|
|
502
|
+
const orchestratorMessage = [
|
|
503
|
+
`[Cross-Machine] from ${msg.fromName} (${msg.from})`,
|
|
504
|
+
`Type: ${msg.type}`,
|
|
505
|
+
msg.payload.message ? `Message: ${msg.payload.message}` : '',
|
|
506
|
+
msg.payload.task ? `Task: ${msg.payload.task}` : '',
|
|
507
|
+
msg.payload.priority ? `Priority: ${msg.payload.priority}` : '',
|
|
508
|
+
msg.payload.context ? `Context: ${msg.payload.context}` : '',
|
|
509
|
+
msg.payload.summary ? `Summary: ${msg.payload.summary}` : '',
|
|
510
|
+
].filter(Boolean).join('\n');
|
|
511
|
+
if (this.messageQueueService) {
|
|
512
|
+
try {
|
|
513
|
+
this.messageQueueService.enqueue({
|
|
514
|
+
content: orchestratorMessage,
|
|
515
|
+
conversationId: `x-machine-${msg.from}-${Date.now()}`,
|
|
516
|
+
source: 'cross-machine',
|
|
517
|
+
sourceMetadata: {
|
|
518
|
+
fromDevice: msg.from,
|
|
519
|
+
fromDeviceName: msg.fromName,
|
|
520
|
+
messageType: msg.type,
|
|
521
|
+
messageId: msg.messageId,
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
this.logger.info('Cross-machine message enqueued to orchestrator');
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
this.logger.error('Failed to enqueue cross-machine message', {
|
|
528
|
+
error: err instanceof Error ? err.message : String(err),
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
this.logger.warn('Cannot route cross-machine message — MessageQueueService not available');
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Parse command from message text
|
|
538
|
+
*
|
|
539
|
+
* Cleans the message and extracts the command intent.
|
|
540
|
+
*
|
|
541
|
+
* @param text - Raw message text
|
|
542
|
+
* @returns Parsed command with intent and parameters
|
|
543
|
+
*/
|
|
544
|
+
parseCommand(text) {
|
|
545
|
+
// Remove bot mention if present
|
|
546
|
+
const cleanedText = text.replace(/<@[A-Z0-9]+>/g, '').trim();
|
|
547
|
+
return {
|
|
548
|
+
intent: parseCommandIntent(cleanedText),
|
|
549
|
+
rawText: cleanedText,
|
|
550
|
+
parameters: this.extractParameters(cleanedText),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Extract parameters from command text
|
|
555
|
+
*
|
|
556
|
+
* @param text - Command text
|
|
557
|
+
* @returns Extracted parameters
|
|
558
|
+
*/
|
|
559
|
+
extractParameters(text) {
|
|
560
|
+
const params = {};
|
|
561
|
+
// Extract quoted strings
|
|
562
|
+
const quotedMatch = text.match(/"([^"]+)"/);
|
|
563
|
+
if (quotedMatch) {
|
|
564
|
+
params.quoted = quotedMatch[1];
|
|
565
|
+
}
|
|
566
|
+
// Extract @mentions
|
|
567
|
+
const mentionMatch = text.match(/@(\w+)/);
|
|
568
|
+
if (mentionMatch) {
|
|
569
|
+
params.mention = mentionMatch[1];
|
|
570
|
+
}
|
|
571
|
+
// Extract project/team names after keywords
|
|
572
|
+
const forMatch = text.match(/(?:for|on|in)\s+(\w+)/i);
|
|
573
|
+
if (forMatch) {
|
|
574
|
+
params.target = forMatch[1];
|
|
575
|
+
}
|
|
576
|
+
return params;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Get help message
|
|
580
|
+
*
|
|
581
|
+
* @returns Formatted help message for Slack
|
|
582
|
+
*/
|
|
583
|
+
getHelpMessage() {
|
|
584
|
+
return `*Crewly Commands:*
|
|
585
|
+
|
|
586
|
+
:clipboard: *Status*
|
|
587
|
+
• \`status\` - Get overall status
|
|
588
|
+
• \`status [project name]\` - Get project status
|
|
589
|
+
• \`how is [team] doing\` - Get team progress
|
|
590
|
+
|
|
591
|
+
:page_facing_up: *Lists*
|
|
592
|
+
• \`list projects\` - Show all projects
|
|
593
|
+
• \`list teams\` - Show all teams
|
|
594
|
+
• \`list agents\` or \`who's working\` - Show active agents
|
|
595
|
+
|
|
596
|
+
:gear: *Actions*
|
|
597
|
+
• \`assign [task] to [agent/team]\` - Assign work
|
|
598
|
+
• \`create task: [description]\` - Create a new task
|
|
599
|
+
• \`pause [agent/team]\` - Pause work
|
|
600
|
+
• \`resume [agent/team]\` - Resume work
|
|
601
|
+
|
|
602
|
+
:speech_balloon: *Chat*
|
|
603
|
+
Just type naturally to chat with the orchestrator!`;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Determine whether an inbound Slack message should be suppressed from
|
|
607
|
+
* auto-Request creation. Returns a reason string when the message should
|
|
608
|
+
* be skipped, or `null` when a Request should be created normally.
|
|
609
|
+
*
|
|
610
|
+
* Three gates (Pipeline-#4 fix, spec
|
|
611
|
+
* 2026-05-05-request-decompose-pipeline-gap.md, Patch D):
|
|
612
|
+
*
|
|
613
|
+
* 1. **Length / acknowledgement gate.** Trimmed text is shorter than
|
|
614
|
+
* {@link MIN_AUTO_REQUEST_TEXT_LENGTH} OR matches the trivial
|
|
615
|
+
* acknowledgement allowlist ({@link TRIVIAL_ACK_PATTERN}). Catches
|
|
616
|
+
* "好的", "ok", "👍", "thx" — class noise that is never actionable.
|
|
617
|
+
*
|
|
618
|
+
* 2. **Thread continuation gate.** When the inbound message is a reply
|
|
619
|
+
* inside an existing thread (`message.threadTs` ≠ `message.ts`) AND a
|
|
620
|
+
* Request already exists for the **parent** message, suppress to
|
|
621
|
+
* prevent every "are we still in this thread?" follow-up from creating
|
|
622
|
+
* a new sibling Request. Existing Request continues to receive
|
|
623
|
+
* orchestrator routing.
|
|
624
|
+
*
|
|
625
|
+
* 3. **File-only / synthetic-title gate.** Messages whose text is a
|
|
626
|
+
* synthetic `[Slack File: …]` placeholder with no narrative are
|
|
627
|
+
* artefacts of slack-bridge file-handling, not user requests.
|
|
628
|
+
*
|
|
629
|
+
* Each gate returns a stable reason string for log auditability. The
|
|
630
|
+
* caller logs at `debug` so suppression decisions are observable without
|
|
631
|
+
* noise.
|
|
632
|
+
*
|
|
633
|
+
* @param message - The incoming Slack message (already validated)
|
|
634
|
+
* @param rawText - The raw text body, trimmed by caller if needed
|
|
635
|
+
* @param svc - The RequestService instance for the thread-continuation lookup
|
|
636
|
+
* @returns A reason string when suppressed; `null` when allowed
|
|
637
|
+
*/
|
|
638
|
+
async shouldSuppressAutoRequest(message, rawText, svc) {
|
|
639
|
+
const trimmed = rawText.trim();
|
|
640
|
+
// Gate 1 — length + ack allowlist
|
|
641
|
+
if (trimmed.length < MIN_AUTO_REQUEST_TEXT_LENGTH || TRIVIAL_ACK_PATTERN.test(trimmed)) {
|
|
642
|
+
return 'trivial_or_short';
|
|
643
|
+
}
|
|
644
|
+
// Gate 3 — file-only synthetic title (run before gate 2 to skip the I/O)
|
|
645
|
+
if (FILE_ONLY_TITLE_PATTERN.test(trimmed)) {
|
|
646
|
+
return 'file_only';
|
|
647
|
+
}
|
|
648
|
+
// Gate 2 — thread continuation: this message is a reply, and the parent
|
|
649
|
+
// message already has a NON-TERMINAL Request. Cheaper than threading
|
|
650
|
+
// detection at the SLA layer.
|
|
651
|
+
//
|
|
652
|
+
// ARCH 2026-05-05 PR #453 review (NC-1): the gate MUST check
|
|
653
|
+
// `parentRequest.status` is non-terminal — otherwise a thread that
|
|
654
|
+
// completed (parent → done via orc_reply / orc_reply_recheck) and then
|
|
655
|
+
// received a NEW actionable user message would silently suppress the new
|
|
656
|
+
// Request, re-opening the user-visible reliability gap that INBOUND-1
|
|
657
|
+
// was built to close. Terminal-parent threads release suppression so the
|
|
658
|
+
// child message creates a fresh Request.
|
|
659
|
+
const threadTs = message.threadTs;
|
|
660
|
+
if (threadTs && threadTs !== message.ts) {
|
|
661
|
+
const parentMsgId = `slack-${message.channelId}-${threadTs}`;
|
|
662
|
+
const parentRequest = await svc.findBySourceConversationItemId(parentMsgId);
|
|
663
|
+
if (parentRequest && !TERMINAL_REQUEST_STATUSES.has(parentRequest.status)) {
|
|
664
|
+
return 'thread_continuation';
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Handle status command
|
|
671
|
+
*
|
|
672
|
+
* @param command - Parsed command
|
|
673
|
+
* @param context - Conversation context
|
|
674
|
+
* @returns Status response
|
|
675
|
+
*/
|
|
676
|
+
async handleStatusCommand(command, context) {
|
|
677
|
+
return await this.sendToOrchestrator(`Give me a brief status update. ${command.rawText}`, context);
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Handle list commands (projects, teams, agents)
|
|
681
|
+
*
|
|
682
|
+
* @param command - Parsed command
|
|
683
|
+
* @param context - Conversation context
|
|
684
|
+
* @returns List response
|
|
685
|
+
*/
|
|
686
|
+
async handleListCommand(command, context) {
|
|
687
|
+
const prompts = {
|
|
688
|
+
list_projects: 'List all active projects with their status.',
|
|
689
|
+
list_teams: 'List all teams and their current assignments.',
|
|
690
|
+
list_agents: 'List all agents, their status, and what they are working on.',
|
|
691
|
+
};
|
|
692
|
+
return await this.sendToOrchestrator(prompts[command.intent] || command.rawText, context);
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Handle control commands (pause, resume)
|
|
696
|
+
*
|
|
697
|
+
* @param command - Parsed command
|
|
698
|
+
* @param context - Conversation context
|
|
699
|
+
* @returns Control response
|
|
700
|
+
*/
|
|
701
|
+
async handleControlCommand(command, context) {
|
|
702
|
+
const action = command.intent === 'pause' ? 'Pause' : 'Resume';
|
|
703
|
+
const target = command.parameters.target || command.parameters.mention || 'all agents';
|
|
704
|
+
return await this.sendToOrchestrator(`${action} ${target}.`, context);
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Attempt to auto-recover the orchestrator with a hard timeout.
|
|
708
|
+
*
|
|
709
|
+
* Calls `triggerOrchestratorSetup()` from the orchestrator service module
|
|
710
|
+
* directly (bypassing HTTP). Wraps the attempt in a 5-second deadline so
|
|
711
|
+
* a wedged setup cannot stall the Slack message ingress thread.
|
|
712
|
+
*
|
|
713
|
+
* Behavior:
|
|
714
|
+
* - Timeout: rejects with a TimeoutError-like message after 5s.
|
|
715
|
+
* - Setup error: rejects with the underlying error.
|
|
716
|
+
* - Setup returning `success: false`: rejects with the recorded error
|
|
717
|
+
* (the caller treats this the same as a thrown error and falls
|
|
718
|
+
* through to the offline path).
|
|
719
|
+
*
|
|
720
|
+
* Caller is expected to wrap the call in try/catch and continue down
|
|
721
|
+
* the offline path on rejection — auto-recovery is best-effort.
|
|
722
|
+
*
|
|
723
|
+
* @returns Resolves when setup completes successfully (or was skipped
|
|
724
|
+
* because the orc was already healthy)
|
|
725
|
+
* @throws Error on timeout or setup failure
|
|
726
|
+
* @internal Visible for tests via class member access.
|
|
727
|
+
*/
|
|
728
|
+
async attemptAutoRecovery() {
|
|
729
|
+
let timeoutHandle;
|
|
730
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
731
|
+
timeoutHandle = setTimeout(() => {
|
|
732
|
+
reject(new Error(`auto-recovery setup timed out after ${AUTO_RECOVERY_TIMEOUT_MS}ms`));
|
|
733
|
+
}, AUTO_RECOVERY_TIMEOUT_MS);
|
|
734
|
+
// Don't keep the process alive just for this timer
|
|
735
|
+
timeoutHandle.unref?.();
|
|
736
|
+
});
|
|
737
|
+
try {
|
|
738
|
+
const result = await Promise.race([triggerOrchestratorSetup(), timeoutPromise]);
|
|
739
|
+
if (!result.success) {
|
|
740
|
+
throw new Error(result.error || 'orchestrator setup returned success=false');
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
finally {
|
|
744
|
+
if (timeoutHandle) {
|
|
745
|
+
clearTimeout(timeoutHandle);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Send message to orchestrator via the message queue and wait for response.
|
|
751
|
+
*
|
|
752
|
+
* Checks if the orchestrator is active before sending. Enqueues the message
|
|
753
|
+
* and waits for the response via a resolve callback injected into sourceMetadata.
|
|
754
|
+
*
|
|
755
|
+
* @param message - Message to send
|
|
756
|
+
* @param context - Optional conversation context
|
|
757
|
+
* @returns Orchestrator response or offline/error message
|
|
758
|
+
*/
|
|
759
|
+
async sendToOrchestrator(message, context) {
|
|
760
|
+
try {
|
|
761
|
+
// Check if orchestrator is active before attempting to send
|
|
762
|
+
let isActive = await isOrchestratorActive();
|
|
763
|
+
// Auto-recovery (B0 hot-fix, defense-in-depth):
|
|
764
|
+
// ESTestNode regression — `isActive` falsely reports offline when the
|
|
765
|
+
// orchestrator is an in-process Crewly Agent runtime that has lost its
|
|
766
|
+
// status registration but is otherwise healthy. Before falling
|
|
767
|
+
// through to the offline path, attempt one synchronous setup call.
|
|
768
|
+
// This is intentionally once-per-message (not retry-loop). On
|
|
769
|
+
// success we re-check isActive and proceed normally; on failure we
|
|
770
|
+
// continue down the existing offline branch (Auditor + queue).
|
|
771
|
+
if (!isActive) {
|
|
772
|
+
const recoveryStart = Date.now();
|
|
773
|
+
this.logger.info('Orchestrator offline — attempting auto-recovery via triggerOrchestratorSetup', {
|
|
774
|
+
timeoutMs: AUTO_RECOVERY_TIMEOUT_MS,
|
|
775
|
+
});
|
|
776
|
+
try {
|
|
777
|
+
await this.attemptAutoRecovery();
|
|
778
|
+
isActive = await isOrchestratorActive();
|
|
779
|
+
this.logger.info('Auto-recovery setup attempt complete', {
|
|
780
|
+
elapsedMs: Date.now() - recoveryStart,
|
|
781
|
+
isActiveAfter: isActive,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
catch (err) {
|
|
785
|
+
this.logger.warn('Auto-recovery setup attempt failed (falling through to offline path)', {
|
|
786
|
+
elapsedMs: Date.now() - recoveryStart,
|
|
787
|
+
error: err instanceof Error ? err.message : String(err),
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
if (!isActive) {
|
|
792
|
+
// Fallback: route to Auditor agent if it's active
|
|
793
|
+
const auditorSession = AUDITOR_SCHEDULER_CONSTANTS.AUDITOR_SESSION_NAME;
|
|
794
|
+
const auditorActive = await isAgentActive(auditorSession);
|
|
795
|
+
if (auditorActive) {
|
|
796
|
+
this.logger.info('Orchestrator offline — routing message to Auditor agent');
|
|
797
|
+
// Auditor fallback is NOT an orc reply — caller must not auto-resolve SLA.
|
|
798
|
+
const fallback = await this.sendToAuditorFallback(message, context);
|
|
799
|
+
return { response: fallback, fromOrcReply: false };
|
|
800
|
+
}
|
|
801
|
+
// #247: Queue the message for replay when orchestrator comes back online,
|
|
802
|
+
// instead of silently dropping it. The queue processor defers delivery
|
|
803
|
+
// until the orchestrator registers (agentStatus === 'active').
|
|
804
|
+
// NOTE: This is an offline-path acknowledgement, NOT a real orc reply —
|
|
805
|
+
// every return from this branch must set `fromOrcReply: false`.
|
|
806
|
+
if (this.messageQueueService) {
|
|
807
|
+
this.logger.info('Orchestrator offline — queuing message for replay when it comes back online');
|
|
808
|
+
// Enrich message with thread file path hint for orchestrator context
|
|
809
|
+
let enrichedMessage = message;
|
|
810
|
+
if (this.threadStore && context) {
|
|
811
|
+
const threadFilePath = this.threadStore.getThreadFilePath(context.channelId, context.threadTs);
|
|
812
|
+
enrichedMessage = `${message}\n\n[Thread context file: ${threadFilePath}]`;
|
|
813
|
+
}
|
|
814
|
+
// Store in canonical chat-v2 for persistence
|
|
815
|
+
const result = {
|
|
816
|
+
conversation: {
|
|
817
|
+
id: this.persistSlackInbound({
|
|
818
|
+
content: enrichedMessage,
|
|
819
|
+
conversationId: context?.conversationId,
|
|
820
|
+
agentSession: 'crewly-orc',
|
|
821
|
+
channelId: context?.channelId,
|
|
822
|
+
threadTs: context?.threadTs,
|
|
823
|
+
userId: context?.userId,
|
|
824
|
+
}).conversationId,
|
|
825
|
+
},
|
|
826
|
+
};
|
|
827
|
+
try {
|
|
828
|
+
this.messageQueueService.enqueue({
|
|
829
|
+
content: enrichedMessage,
|
|
830
|
+
conversationId: result.conversation.id,
|
|
831
|
+
source: 'slack',
|
|
832
|
+
sourceMetadata: {
|
|
833
|
+
userId: context?.userId,
|
|
834
|
+
channelId: context?.channelId,
|
|
835
|
+
threadTs: context?.threadTs,
|
|
836
|
+
},
|
|
837
|
+
});
|
|
838
|
+
// Track inbound thread for offline message recovery
|
|
839
|
+
if (this.threadStatusQueue && context?.channelId) {
|
|
840
|
+
try {
|
|
841
|
+
const threadKey = context.threadTs
|
|
842
|
+
? `${context.channelId}:${context.threadTs}`
|
|
843
|
+
: `${context.channelId}:${Date.now()}`;
|
|
844
|
+
this.threadStatusQueue.trackInbound({
|
|
845
|
+
source: 'slack',
|
|
846
|
+
threadKey,
|
|
847
|
+
conversationId: result.conversation.id,
|
|
848
|
+
messagePreview: message.slice(0, THREAD_STATUS_CONSTANTS.MAX_PREVIEW_LENGTH),
|
|
849
|
+
sourceMetadata: {
|
|
850
|
+
channelId: context.channelId,
|
|
851
|
+
threadTs: context.threadTs,
|
|
852
|
+
userId: context.userId,
|
|
853
|
+
},
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
catch (trackErr) {
|
|
857
|
+
this.logger.warn('Failed to track inbound thread status (offline path)', {
|
|
858
|
+
error: trackErr instanceof Error ? trackErr.message : String(trackErr),
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return {
|
|
863
|
+
response: 'The orchestrator is currently offline. Your message has been queued and will be processed when it comes back online.',
|
|
864
|
+
fromOrcReply: false,
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
catch (enqueueErr) {
|
|
868
|
+
this.logger.warn('Failed to queue message for offline orchestrator', {
|
|
869
|
+
error: enqueueErr instanceof Error ? enqueueErr.message : String(enqueueErr),
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
this.logger.info('Orchestrator is not active, returning offline message');
|
|
874
|
+
return { response: getOrchestratorOfflineMessage(true), fromOrcReply: false };
|
|
875
|
+
}
|
|
876
|
+
// Check if message queue service is available
|
|
877
|
+
if (!this.messageQueueService) {
|
|
878
|
+
this.logger.error('Message queue service not configured');
|
|
879
|
+
return {
|
|
880
|
+
response: 'The Slack bridge is not properly configured. Please restart the server.',
|
|
881
|
+
fromOrcReply: false,
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
// Enrich message with thread file path hint for orchestrator context
|
|
885
|
+
let enrichedMessage = message;
|
|
886
|
+
if (this.threadStore && context) {
|
|
887
|
+
const threadFilePath = this.threadStore.getThreadFilePath(context.channelId, context.threadTs);
|
|
888
|
+
enrichedMessage = `${message}\n\n[Thread context file: ${threadFilePath}]`;
|
|
889
|
+
}
|
|
890
|
+
// Persist user message in canonical chat-v2 store
|
|
891
|
+
const result = {
|
|
892
|
+
conversation: {
|
|
893
|
+
id: this.persistSlackInbound({
|
|
894
|
+
content: enrichedMessage,
|
|
895
|
+
conversationId: context?.conversationId,
|
|
896
|
+
agentSession: 'crewly-orc',
|
|
897
|
+
channelId: context?.channelId,
|
|
898
|
+
threadTs: context?.threadTs,
|
|
899
|
+
userId: context?.userId,
|
|
900
|
+
}).conversationId,
|
|
901
|
+
},
|
|
902
|
+
};
|
|
903
|
+
// Enqueue the message with a resolve callback for response routing.
|
|
904
|
+
// The QueueProcessorService will call slackResolve() when the
|
|
905
|
+
// orchestrator responds, unblocking this promise.
|
|
906
|
+
//
|
|
907
|
+
// The promise resolves with `{response, fromOrcReply}`. `fromOrcReply`
|
|
908
|
+
// is `true` ONLY when slackResolve fires (orc's reply-slack skill
|
|
909
|
+
// delivered an actual reply). On responseTimeoutMs or enqueue failure
|
|
910
|
+
// it is `false` — the placeholder must NOT auto-resolve the SLA
|
|
911
|
+
// tracker (Steve 2026-04-30 incident: false-done Request cascade).
|
|
912
|
+
const orcResponse = await new Promise((resolve) => {
|
|
913
|
+
let resolved = false;
|
|
914
|
+
const timeoutId = setTimeout(() => {
|
|
915
|
+
if (!resolved) {
|
|
916
|
+
resolved = true;
|
|
917
|
+
resolve({
|
|
918
|
+
response: 'The orchestrator is still processing your request. It will reply here when ready — no need to resend.',
|
|
919
|
+
fromOrcReply: false,
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
}, this.config.responseTimeoutMs);
|
|
923
|
+
try {
|
|
924
|
+
this.messageQueueService.enqueue({
|
|
925
|
+
content: enrichedMessage,
|
|
926
|
+
conversationId: result.conversation.id,
|
|
927
|
+
source: 'slack',
|
|
928
|
+
sourceMetadata: {
|
|
929
|
+
slackResolve: (resp) => {
|
|
930
|
+
if (!resolved) {
|
|
931
|
+
resolved = true;
|
|
932
|
+
clearTimeout(timeoutId);
|
|
933
|
+
// 2026-05-12 dogfood bug: the QueueProcessor fires
|
|
934
|
+
// `routeResponse(message, '')` immediately after PTY
|
|
935
|
+
// delivery to unblock this promise (see
|
|
936
|
+
// queue-processor.service.ts:692 "fire-and-forget"
|
|
937
|
+
// comment). That call carries an EMPTY response — orc
|
|
938
|
+
// has only RECEIVED the message, not REPLIED. The
|
|
939
|
+
// previous shape of this callback always returned
|
|
940
|
+
// `fromOrcReply: true`, so every Slack delivery
|
|
941
|
+
// triggered `markResolvedByThread('orc_reply')` →
|
|
942
|
+
// `maybeCloseRequest` → Request cascaded to `done`
|
|
943
|
+
// with `result: "Auto-closed by SLA: orc_reply"`
|
|
944
|
+
// before the user ever saw a reply.
|
|
945
|
+
//
|
|
946
|
+
// Real orc replies (via the reply-slack skill) come
|
|
947
|
+
// through a different route — the orc invokes
|
|
948
|
+
// `POST /slack/send`, which posts to Slack directly;
|
|
949
|
+
// the SLA tracker is resolved when the
|
|
950
|
+
// `respond_to_user` WI transitions to `verified` and
|
|
951
|
+
// the cascade subscriber picks it up. That path
|
|
952
|
+
// produces a non-empty response payload only when
|
|
953
|
+
// a worker explicitly drives reply text back through
|
|
954
|
+
// the queue (rare; most orc replies are direct API).
|
|
955
|
+
//
|
|
956
|
+
// Gate: treat empty resp as a delivery ack ONLY
|
|
957
|
+
// (fromOrcReply=false). Non-empty resp keeps the
|
|
958
|
+
// legacy `true` semantics for any worker path that
|
|
959
|
+
// does echo a reply through the queue.
|
|
960
|
+
const fromOrcReply = resp.length > 0;
|
|
961
|
+
resolve({ response: resp, fromOrcReply });
|
|
962
|
+
}
|
|
963
|
+
},
|
|
964
|
+
userId: context?.userId,
|
|
965
|
+
channelId: context?.channelId,
|
|
966
|
+
threadTs: context?.threadTs,
|
|
967
|
+
},
|
|
968
|
+
});
|
|
969
|
+
// Track inbound thread for lifecycle monitoring and restart recovery
|
|
970
|
+
if (this.threadStatusQueue && context?.channelId) {
|
|
971
|
+
try {
|
|
972
|
+
const threadKey = context.threadTs
|
|
973
|
+
? `${context.channelId}:${context.threadTs}`
|
|
974
|
+
: `${context.channelId}:${Date.now()}`;
|
|
975
|
+
this.threadStatusQueue.trackInbound({
|
|
976
|
+
source: 'slack',
|
|
977
|
+
threadKey,
|
|
978
|
+
conversationId: result.conversation.id,
|
|
979
|
+
messagePreview: message.slice(0, THREAD_STATUS_CONSTANTS.MAX_PREVIEW_LENGTH),
|
|
980
|
+
sourceMetadata: {
|
|
981
|
+
channelId: context.channelId,
|
|
982
|
+
threadTs: context.threadTs,
|
|
983
|
+
userId: context.userId,
|
|
984
|
+
},
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
catch (trackErr) {
|
|
988
|
+
this.logger.warn('Failed to track inbound thread status', {
|
|
989
|
+
error: trackErr instanceof Error ? trackErr.message : String(trackErr),
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
catch (enqueueErr) {
|
|
995
|
+
if (!resolved) {
|
|
996
|
+
resolved = true;
|
|
997
|
+
clearTimeout(timeoutId);
|
|
998
|
+
resolve({
|
|
999
|
+
response: `Failed to enqueue message: ${enqueueErr instanceof Error ? enqueueErr.message : String(enqueueErr)}`,
|
|
1000
|
+
fromOrcReply: false,
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
return orcResponse;
|
|
1006
|
+
}
|
|
1007
|
+
catch (error) {
|
|
1008
|
+
this.logger.error('Error sending to orchestrator', { error: error instanceof Error ? error.message : String(error) });
|
|
1009
|
+
throw error;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Route a message to the Auditor agent as a fallback when the orchestrator is offline.
|
|
1014
|
+
*
|
|
1015
|
+
* Enqueues the message targeting the Auditor's session so the queue processor
|
|
1016
|
+
* delivers it to the Auditor PTY instead of the orchestrator. Includes Slack
|
|
1017
|
+
* context (channel, thread) so the Auditor can respond.
|
|
1018
|
+
*
|
|
1019
|
+
* **Thread file enrichment (parity with `sendToOrchestrator` ~line 712):**
|
|
1020
|
+
* When a `threadStore` and Slack `context` are available, the enriched
|
|
1021
|
+
* message also carries a `[Thread context file: <path>]` line. Without
|
|
1022
|
+
* this, the Auditor receives only the bare `[SLACK_CONTEXT:...]` prefix
|
|
1023
|
+
* and cannot read prior thread history, causing context-blind replies.
|
|
1024
|
+
* This is the bug Steve hit on 2026-04-25 (StevesPrompt deploy thread):
|
|
1025
|
+
* orchestrator dropped, Slack fell through here, Auditor responded with
|
|
1026
|
+
* "I cannot see the rest of the conversation" because the thread file
|
|
1027
|
+
* pointer was missing. Bringing the enrichment in line with the
|
|
1028
|
+
* orchestrator path closes the gap.
|
|
1029
|
+
*
|
|
1030
|
+
* @param message - Original user message
|
|
1031
|
+
* @param context - Slack conversation context
|
|
1032
|
+
* @returns Acknowledgement message to the user
|
|
1033
|
+
*/
|
|
1034
|
+
async sendToAuditorFallback(message, context) {
|
|
1035
|
+
const auditorSession = AUDITOR_SCHEDULER_CONSTANTS.AUDITOR_SESSION_NAME;
|
|
1036
|
+
// Build a context-enriched message so the Auditor knows the source
|
|
1037
|
+
const slackPrefix = context
|
|
1038
|
+
? `[SLACK_CONTEXT:channelId=${context.channelId},threadTs=${context.threadTs || ''}]`
|
|
1039
|
+
: '';
|
|
1040
|
+
let enrichedMessage = `${slackPrefix} [FALLBACK] Orchestrator is offline. User message:\n${message}`;
|
|
1041
|
+
if (this.threadStore && context) {
|
|
1042
|
+
const threadFilePath = this.threadStore.getThreadFilePath(context.channelId, context.threadTs);
|
|
1043
|
+
enrichedMessage += `\n\n[Thread context file: ${threadFilePath}]`;
|
|
1044
|
+
}
|
|
1045
|
+
// Try to enqueue via the message queue (same path as orchestrator delivery)
|
|
1046
|
+
if (this.messageQueueService) {
|
|
1047
|
+
try {
|
|
1048
|
+
const result = {
|
|
1049
|
+
conversation: {
|
|
1050
|
+
id: this.persistSlackInbound({
|
|
1051
|
+
content: enrichedMessage,
|
|
1052
|
+
conversationId: context?.conversationId,
|
|
1053
|
+
agentSession: auditorSession,
|
|
1054
|
+
channelId: context?.channelId,
|
|
1055
|
+
threadTs: context?.threadTs,
|
|
1056
|
+
userId: context?.userId,
|
|
1057
|
+
}).conversationId,
|
|
1058
|
+
},
|
|
1059
|
+
};
|
|
1060
|
+
this.messageQueueService.enqueue({
|
|
1061
|
+
content: enrichedMessage,
|
|
1062
|
+
conversationId: result.conversation.id,
|
|
1063
|
+
source: 'slack',
|
|
1064
|
+
targetSession: auditorSession,
|
|
1065
|
+
sourceMetadata: {
|
|
1066
|
+
slackResolve: undefined,
|
|
1067
|
+
userId: context?.userId,
|
|
1068
|
+
channelId: context?.channelId,
|
|
1069
|
+
},
|
|
1070
|
+
});
|
|
1071
|
+
this.logger.info('Message routed to Auditor agent via queue', { auditorSession });
|
|
1072
|
+
return 'The orchestrator is currently offline. Your message has been forwarded to the Auditor agent.';
|
|
1073
|
+
}
|
|
1074
|
+
catch (err) {
|
|
1075
|
+
this.logger.error('Failed to route message to Auditor via queue', {
|
|
1076
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
// Last resort: direct delivery via AuditorSchedulerService
|
|
1081
|
+
try {
|
|
1082
|
+
const { AuditorSchedulerService } = await import('../agent/auditor-scheduler.service.js');
|
|
1083
|
+
const scheduler = AuditorSchedulerService.getInstance();
|
|
1084
|
+
await scheduler.handleUserMessage(`[FALLBACK] Orchestrator is offline. User message: ${message}`, { channelId: context?.channelId || '', threadTs: context?.threadTs || '' });
|
|
1085
|
+
return 'The orchestrator is currently offline. Your message has been forwarded to the Auditor agent.';
|
|
1086
|
+
}
|
|
1087
|
+
catch (err) {
|
|
1088
|
+
this.logger.error('Failed to route message to Auditor via scheduler', {
|
|
1089
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
return getOrchestratorOfflineMessage(true);
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Refresh file download URLs via the Slack files.info API.
|
|
1096
|
+
* The event payload URLs may not work with just the bot token — the
|
|
1097
|
+
* files.info API returns authenticated URLs and validates scope.
|
|
1098
|
+
*
|
|
1099
|
+
* @param files - Slack file objects to refresh URLs for (mutated in place)
|
|
1100
|
+
* @returns true if downloads can proceed, false if scope is missing
|
|
1101
|
+
*/
|
|
1102
|
+
async refreshFileUrls(files) {
|
|
1103
|
+
for (const file of files) {
|
|
1104
|
+
try {
|
|
1105
|
+
const freshInfo = await this.slackService.getFileInfo(file.id);
|
|
1106
|
+
if (freshInfo.url_private_download) {
|
|
1107
|
+
file.url_private_download = freshInfo.url_private_download;
|
|
1108
|
+
}
|
|
1109
|
+
if (freshInfo.url_private) {
|
|
1110
|
+
file.url_private = freshInfo.url_private;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
catch (err) {
|
|
1114
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1115
|
+
if (errMsg.includes('missing_scope') || errMsg.includes('not_allowed_token_type')) {
|
|
1116
|
+
this.logger.error('Bot token lacks files:read scope — cannot download files. Add the "files:read" scope to your Slack app.');
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
this.logger.warn('Could not refresh URL via files.info', { fileName: file.name, error: errMsg });
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return true;
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Download image files from a Slack message using SlackImageService.
|
|
1126
|
+
* Populates `message.images` with successfully downloaded image info.
|
|
1127
|
+
* Downloads are batched with a concurrency limit of MAX_CONCURRENT_DOWNLOADS.
|
|
1128
|
+
*
|
|
1129
|
+
* @param message - Incoming message to attach downloaded images to
|
|
1130
|
+
* @param imageFiles - Pre-filtered list of image files to download
|
|
1131
|
+
*/
|
|
1132
|
+
async downloadMessageImages(message, imageFiles) {
|
|
1133
|
+
const botToken = this.slackService.getBotToken();
|
|
1134
|
+
if (!botToken) {
|
|
1135
|
+
this.logger.warn('Cannot download images: no bot token available');
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
if (imageFiles.length === 0) {
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
const slackImageService = getSlackImageService();
|
|
1142
|
+
const files = imageFiles;
|
|
1143
|
+
const downloadedImages = [];
|
|
1144
|
+
const maxConcurrent = SLACK_IMAGE_CONSTANTS.MAX_CONCURRENT_DOWNLOADS;
|
|
1145
|
+
const rejectionMessages = [];
|
|
1146
|
+
// Refresh file URLs via files.info API before downloading.
|
|
1147
|
+
const canProceed = await this.refreshFileUrls(files);
|
|
1148
|
+
if (!canProceed)
|
|
1149
|
+
return;
|
|
1150
|
+
for (let i = 0; i < files.length; i += maxConcurrent) {
|
|
1151
|
+
const batch = files.slice(i, i + maxConcurrent);
|
|
1152
|
+
const results = await Promise.allSettled(batch.map((file) => slackImageService.downloadImage(file, botToken)));
|
|
1153
|
+
for (let j = 0; j < results.length; j++) {
|
|
1154
|
+
const result = results[j];
|
|
1155
|
+
if (result.status === 'fulfilled') {
|
|
1156
|
+
downloadedImages.push(result.value);
|
|
1157
|
+
}
|
|
1158
|
+
else {
|
|
1159
|
+
const fileName = batch[j].name;
|
|
1160
|
+
const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
1161
|
+
this.logger.warn('Failed to download image', { fileName, reason });
|
|
1162
|
+
// Track files rejected due to validation (size/type) to warn the Slack user.
|
|
1163
|
+
// These match the error prefixes from SlackImageService.downloadImage().
|
|
1164
|
+
if (reason.startsWith('File too large:') || reason.startsWith('Unsupported image type:')) {
|
|
1165
|
+
rejectionMessages.push(`${fileName}: ${reason}`);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
if (downloadedImages.length > 0) {
|
|
1171
|
+
message.images = downloadedImages;
|
|
1172
|
+
}
|
|
1173
|
+
// Send a warning back to Slack if any files were rejected
|
|
1174
|
+
if (rejectionMessages.length > 0) {
|
|
1175
|
+
try {
|
|
1176
|
+
const maxMB = Math.round(SLACK_IMAGE_CONSTANTS.MAX_FILE_SIZE / (1024 * 1024));
|
|
1177
|
+
const supportedTypes = SLACK_IMAGE_CONSTANTS.SUPPORTED_MIMES.map(m => m.replace('image/', '').toUpperCase()).join(', ');
|
|
1178
|
+
const warningText = `:warning: Some image(s) could not be processed:\n${rejectionMessages.map(f => `• ${f}`).join('\n')}\n\nSupported types: ${supportedTypes}. Max size: ${maxMB} MB.`;
|
|
1179
|
+
await this.slackService.sendMessage({
|
|
1180
|
+
channelId: message.channelId,
|
|
1181
|
+
text: warningText,
|
|
1182
|
+
threadTs: message.threadTs || message.ts,
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
catch (err) {
|
|
1186
|
+
this.logger.warn('Failed to send rejection warning to Slack', { error: err instanceof Error ? err.message : String(err) });
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Download non-image file attachments from a Slack message.
|
|
1192
|
+
* Uses authenticated fetch (same pattern as SlackImageService) to download
|
|
1193
|
+
* files to a temp directory. Populates `message.attachments` with results.
|
|
1194
|
+
* Skips magic-byte validation since these are generic files.
|
|
1195
|
+
*
|
|
1196
|
+
* @param message - Incoming message to populate with downloaded file info
|
|
1197
|
+
* @param nonImageFiles - Non-image SlackFile objects to download
|
|
1198
|
+
*/
|
|
1199
|
+
async downloadMessageFiles(message, nonImageFiles) {
|
|
1200
|
+
const botToken = this.slackService.getBotToken();
|
|
1201
|
+
if (!botToken) {
|
|
1202
|
+
this.logger.warn('Cannot download files: no bot token available');
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const crewlyHome = path.join(os.homedir(), '.crewly');
|
|
1206
|
+
const tempDir = path.join(crewlyHome, SLACK_FILE_DOWNLOAD_CONSTANTS.TEMP_DIR);
|
|
1207
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
1208
|
+
const downloadedFiles = [];
|
|
1209
|
+
const rejectionMessages = [];
|
|
1210
|
+
const maxConcurrent = SLACK_FILE_DOWNLOAD_CONSTANTS.MAX_CONCURRENT_DOWNLOADS;
|
|
1211
|
+
// Refresh file URLs via files.info API
|
|
1212
|
+
const canProceed = await this.refreshFileUrls(nonImageFiles);
|
|
1213
|
+
if (!canProceed)
|
|
1214
|
+
return;
|
|
1215
|
+
for (let i = 0; i < nonImageFiles.length; i += maxConcurrent) {
|
|
1216
|
+
const batch = nonImageFiles.slice(i, i + maxConcurrent);
|
|
1217
|
+
const results = await Promise.allSettled(batch.map(async (file) => {
|
|
1218
|
+
// Size check
|
|
1219
|
+
if (file.size > SLACK_FILE_DOWNLOAD_CONSTANTS.MAX_FILE_SIZE) {
|
|
1220
|
+
const maxMB = Math.round(SLACK_FILE_DOWNLOAD_CONSTANTS.MAX_FILE_SIZE / (1024 * 1024));
|
|
1221
|
+
throw new Error(`File too large: ${file.size} bytes (max ${maxMB} MB)`);
|
|
1222
|
+
}
|
|
1223
|
+
const safeName = file.name.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
1224
|
+
const localPath = path.join(tempDir, `${file.id}-${safeName}`);
|
|
1225
|
+
const downloadUrl = file.url_private_download || file.url_private;
|
|
1226
|
+
// Authenticated fetch with manual redirect handling, timeout, and resource cleanup
|
|
1227
|
+
const maxRedirects = SLACK_FILE_DOWNLOAD_CONSTANTS.MAX_DOWNLOAD_REDIRECTS;
|
|
1228
|
+
const timeoutMs = SLACK_FILE_DOWNLOAD_CONSTANTS.DOWNLOAD_TIMEOUT_MS;
|
|
1229
|
+
const controller = new AbortController();
|
|
1230
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1231
|
+
let currentUrl = downloadUrl;
|
|
1232
|
+
let response = null;
|
|
1233
|
+
try {
|
|
1234
|
+
let exhaustedRedirects = true;
|
|
1235
|
+
for (let r = 0; r <= maxRedirects; r++) {
|
|
1236
|
+
response = await fetch(currentUrl, {
|
|
1237
|
+
headers: { 'Authorization': `Bearer ${botToken}` },
|
|
1238
|
+
redirect: 'manual',
|
|
1239
|
+
signal: controller.signal,
|
|
1240
|
+
});
|
|
1241
|
+
if (response.status >= 300 && response.status < 400) {
|
|
1242
|
+
const location = response.headers.get('location');
|
|
1243
|
+
if (!location)
|
|
1244
|
+
throw new Error(`Redirect without Location header`);
|
|
1245
|
+
// Drain response body to free the socket before following redirect
|
|
1246
|
+
await response.body?.cancel();
|
|
1247
|
+
currentUrl = location;
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
exhaustedRedirects = false;
|
|
1251
|
+
break;
|
|
1252
|
+
}
|
|
1253
|
+
if (exhaustedRedirects) {
|
|
1254
|
+
throw new Error(`Too many redirects (${maxRedirects}) while downloading file from Slack`);
|
|
1255
|
+
}
|
|
1256
|
+
if (!response || !response.ok) {
|
|
1257
|
+
throw new Error(`Download failed with status ${response?.status || 'unknown'}`);
|
|
1258
|
+
}
|
|
1259
|
+
if (!response.body) {
|
|
1260
|
+
throw new Error('Empty response body from Slack');
|
|
1261
|
+
}
|
|
1262
|
+
const fileStream = createWriteStream(localPath);
|
|
1263
|
+
const nodeStream = Readable.fromWeb(response.body);
|
|
1264
|
+
await pipeline(nodeStream, fileStream);
|
|
1265
|
+
}
|
|
1266
|
+
catch (downloadErr) {
|
|
1267
|
+
// Clean up partial file on failure
|
|
1268
|
+
try {
|
|
1269
|
+
await fs.unlink(localPath);
|
|
1270
|
+
}
|
|
1271
|
+
catch { /* ignore if file doesn't exist */ }
|
|
1272
|
+
throw downloadErr;
|
|
1273
|
+
}
|
|
1274
|
+
finally {
|
|
1275
|
+
clearTimeout(timeoutId);
|
|
1276
|
+
}
|
|
1277
|
+
// Extract text from PDFs after download
|
|
1278
|
+
let extractedText;
|
|
1279
|
+
if (SLACK_FILE_DOWNLOAD_CONSTANTS.EXTRACTABLE_MIMES.includes(file.mimetype)) {
|
|
1280
|
+
extractedText = await this.extractPdfText(localPath, file.name);
|
|
1281
|
+
}
|
|
1282
|
+
return {
|
|
1283
|
+
id: file.id,
|
|
1284
|
+
name: file.name,
|
|
1285
|
+
mimetype: file.mimetype,
|
|
1286
|
+
localPath,
|
|
1287
|
+
size: file.size,
|
|
1288
|
+
permalink: file.permalink,
|
|
1289
|
+
extractedText,
|
|
1290
|
+
};
|
|
1291
|
+
}));
|
|
1292
|
+
for (let j = 0; j < results.length; j++) {
|
|
1293
|
+
const result = results[j];
|
|
1294
|
+
if (result.status === 'fulfilled') {
|
|
1295
|
+
downloadedFiles.push(result.value);
|
|
1296
|
+
}
|
|
1297
|
+
else {
|
|
1298
|
+
const fileName = batch[j].name;
|
|
1299
|
+
const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
1300
|
+
this.logger.warn('Failed to download file', { fileName, reason });
|
|
1301
|
+
if (reason.startsWith('File too large:')) {
|
|
1302
|
+
rejectionMessages.push(`${fileName}: ${reason}`);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
if (downloadedFiles.length > 0) {
|
|
1308
|
+
message.attachments = downloadedFiles;
|
|
1309
|
+
}
|
|
1310
|
+
if (rejectionMessages.length > 0) {
|
|
1311
|
+
try {
|
|
1312
|
+
const maxMB = Math.round(SLACK_FILE_DOWNLOAD_CONSTANTS.MAX_FILE_SIZE / (1024 * 1024));
|
|
1313
|
+
const warningText = `:warning: Some file(s) could not be downloaded:\n${rejectionMessages.map(f => `• ${f}`).join('\n')}\n\nMax size: ${maxMB} MB.`;
|
|
1314
|
+
await this.slackService.sendMessage({
|
|
1315
|
+
channelId: message.channelId,
|
|
1316
|
+
text: warningText,
|
|
1317
|
+
threadTs: message.threadTs || message.ts,
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
catch (err) {
|
|
1321
|
+
this.logger.warn('Failed to send file rejection warning to Slack', { error: err instanceof Error ? err.message : String(err) });
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Enrich message text with file path references so agents
|
|
1327
|
+
* can read them via their file-reading tools (Claude Code Read, Gemini @file).
|
|
1328
|
+
* Includes both images and non-image file attachments.
|
|
1329
|
+
*
|
|
1330
|
+
* @param message - Message with optional downloaded images and files
|
|
1331
|
+
* @returns Enriched text with file references appended
|
|
1332
|
+
*/
|
|
1333
|
+
enrichTextWithFiles(message) {
|
|
1334
|
+
let text = message.text || '';
|
|
1335
|
+
if (message.images && message.images.length > 0) {
|
|
1336
|
+
for (const img of message.images) {
|
|
1337
|
+
const dims = img.width && img.height ? ` (${img.width}x${img.height})` : '';
|
|
1338
|
+
text += `\n[Slack Image: ${img.localPath}${dims}, ${img.mimetype}]`;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
if (message.attachments && message.attachments.length > 0) {
|
|
1342
|
+
for (const file of message.attachments) {
|
|
1343
|
+
const sizeStr = file.size < 1024 ? `${file.size}B` : `${Math.round(file.size / 1024)}KB`;
|
|
1344
|
+
text += `\n[Slack File: ${file.localPath} (${file.name}, ${file.mimetype}, ${sizeStr})]`;
|
|
1345
|
+
if (file.extractedText) {
|
|
1346
|
+
text += `\n--- Extracted content from ${file.name} ---\n${file.extractedText}\n--- End of ${file.name} ---`;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
return text;
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Extract text content from a downloaded PDF file.
|
|
1354
|
+
* Returns undefined if extraction fails or produces no text.
|
|
1355
|
+
*
|
|
1356
|
+
* @param filePath - Absolute path to the downloaded PDF
|
|
1357
|
+
* @param fileName - Original file name (for logging)
|
|
1358
|
+
* @returns Extracted text (possibly truncated) or undefined
|
|
1359
|
+
*/
|
|
1360
|
+
async extractPdfText(filePath, fileName) {
|
|
1361
|
+
try {
|
|
1362
|
+
const fileBuffer = await fs.readFile(filePath);
|
|
1363
|
+
const parser = new PDFParse({ data: fileBuffer });
|
|
1364
|
+
const pdfData = await parser.getText();
|
|
1365
|
+
const rawText = typeof pdfData === 'string' ? pdfData : pdfData?.text;
|
|
1366
|
+
if (rawText && rawText.trim().length > 0) {
|
|
1367
|
+
const maxLen = SLACK_FILE_DOWNLOAD_CONSTANTS.MAX_EXTRACTED_TEXT_LENGTH;
|
|
1368
|
+
return rawText.trim().length > maxLen
|
|
1369
|
+
? rawText.trim().substring(0, maxLen) + '\n... [truncated]'
|
|
1370
|
+
: rawText.trim();
|
|
1371
|
+
}
|
|
1372
|
+
return undefined;
|
|
1373
|
+
}
|
|
1374
|
+
catch (extractErr) {
|
|
1375
|
+
this.logger.warn('Failed to extract text from PDF', {
|
|
1376
|
+
fileName,
|
|
1377
|
+
error: extractErr instanceof Error ? extractErr.message : String(extractErr),
|
|
1378
|
+
});
|
|
1379
|
+
return undefined;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Add typing indicator to message
|
|
1384
|
+
*
|
|
1385
|
+
* @param message - Message to add indicator to
|
|
1386
|
+
*/
|
|
1387
|
+
async addTypingIndicator(message) {
|
|
1388
|
+
try {
|
|
1389
|
+
await this.slackService.addReaction(message.channelId, message.ts, 'eyes');
|
|
1390
|
+
}
|
|
1391
|
+
catch (error) {
|
|
1392
|
+
// Non-critical - reactions require 'reactions:write' scope which is optional
|
|
1393
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1394
|
+
if (errorMessage.includes('missing_scope')) {
|
|
1395
|
+
// Only log once about missing scope, not on every message
|
|
1396
|
+
if (!this.loggedMissingScope) {
|
|
1397
|
+
this.logger.warn('Reactions disabled - add reactions:write scope to Slack app for typing indicators');
|
|
1398
|
+
this.loggedMissingScope = true;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
else {
|
|
1402
|
+
this.logger.warn('Could not add typing indicator', { error: errorMessage });
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Mark message as complete
|
|
1408
|
+
*
|
|
1409
|
+
* @param message - Message to mark complete
|
|
1410
|
+
*/
|
|
1411
|
+
async markComplete(message) {
|
|
1412
|
+
try {
|
|
1413
|
+
await this.slackService.addReaction(message.channelId, message.ts, 'white_check_mark');
|
|
1414
|
+
}
|
|
1415
|
+
catch (error) {
|
|
1416
|
+
// Non-critical - reactions require 'reactions:write' scope which is optional
|
|
1417
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1418
|
+
if (!errorMessage.includes('missing_scope')) {
|
|
1419
|
+
this.logger.warn('Could not add completion indicator', { error: errorMessage });
|
|
1420
|
+
}
|
|
1421
|
+
// Silent fail for missing_scope since we already logged about it
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Add a completion reaction (✅) to a message that was previously
|
|
1426
|
+
* stored as a pending reaction. Consumes the pending reaction.
|
|
1427
|
+
*
|
|
1428
|
+
* @param channelId - Slack channel ID
|
|
1429
|
+
* @param threadTs - Thread timestamp (key for lookup)
|
|
1430
|
+
*/
|
|
1431
|
+
async addCompletionReaction(channelId, threadTs) {
|
|
1432
|
+
const key = `${channelId}:${threadTs}`;
|
|
1433
|
+
const messageTs = this.pendingReactions.get(key);
|
|
1434
|
+
if (messageTs) {
|
|
1435
|
+
// Consume the pending reaction
|
|
1436
|
+
this.pendingReactions.delete(key);
|
|
1437
|
+
try {
|
|
1438
|
+
await this.slackService.addReaction(channelId, messageTs, 'white_check_mark');
|
|
1439
|
+
}
|
|
1440
|
+
catch (error) {
|
|
1441
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1442
|
+
// Suppress "already_reacted" errors as they are common and non-fatal
|
|
1443
|
+
if (errMsg.includes('already_reacted')) {
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
// Suppress "missing_scope" since we already log/warn about that elsewhere
|
|
1447
|
+
if (!errMsg.includes('missing_scope')) {
|
|
1448
|
+
this.logger.warn('Failed to add completion reaction', { channelId, messageTs, error: errMsg });
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Record response to thread store. Slack delivery is handled exclusively
|
|
1455
|
+
* by the reply-slack skill via the API — no terminal output fallback needed.
|
|
1456
|
+
*
|
|
1457
|
+
* The `fromOrcReply` flag gates the V3 SLA auto-resolve hook
|
|
1458
|
+
* (`markResolvedByThread`). Only real orc replies (delivered via the
|
|
1459
|
+
* orc's reply-slack skill → `slackResolve` callback) may auto-resolve
|
|
1460
|
+
* the SLA tracker. Placeholder / timeout / offline / fallback responses
|
|
1461
|
+
* must NOT trigger the cascade — doing so closes the parent Request to
|
|
1462
|
+
* `done` even though the orc never actually replied (Steve 2026-04-30
|
|
1463
|
+
* incident: false-done Request, paired with Bug 1 silent-drop in
|
|
1464
|
+
* agent-registration.service.ts).
|
|
1465
|
+
*
|
|
1466
|
+
* Default `fromOrcReply = true` preserves the legacy behaviour for the
|
|
1467
|
+
* mention-routing call site (line ~327) which currently has no
|
|
1468
|
+
* placeholder semantics — its `sendToAgent` path delivers a direct
|
|
1469
|
+
* agent reply when reachable. If that path later grows a timeout
|
|
1470
|
+
* fallback, the call site should pass `false` explicitly.
|
|
1471
|
+
*
|
|
1472
|
+
* @param originalMessage - Original incoming message
|
|
1473
|
+
* @param response - Response content (real reply or placeholder)
|
|
1474
|
+
* @param fromOrcReply - `true` only when the response came from a
|
|
1475
|
+
* real agent/orc reply path. `false` for any timeout / fallback /
|
|
1476
|
+
* offline acknowledgement. Drives whether the SLA tracker is
|
|
1477
|
+
* auto-resolved for the matching Slack thread.
|
|
1478
|
+
*/
|
|
1479
|
+
async sendSlackResponse(originalMessage, response, fromOrcReply = true) {
|
|
1480
|
+
await this.recordThreadReply(originalMessage, response);
|
|
1481
|
+
// INBOUND-1.4: notify the RequestSlaSubscriber that the orc replied to
|
|
1482
|
+
// this Slack thread so any tracked respond_to_user WorkItem can
|
|
1483
|
+
// auto-transition and the SLA timers no-op. Lazy import breaks the
|
|
1484
|
+
// static cycle (subscriber boot wires this bridge).
|
|
1485
|
+
if (!fromOrcReply) {
|
|
1486
|
+
this.logger.debug('Skipping SLA auto-resolve — response is not a real orc reply', {
|
|
1487
|
+
threadTs: originalMessage.ts,
|
|
1488
|
+
responsePreview: response.slice(0, 60),
|
|
1489
|
+
});
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
try {
|
|
1493
|
+
const { getRequestSlaSubscriber } = await import('../v3/request-sla.subscriber.js');
|
|
1494
|
+
const sub = getRequestSlaSubscriber();
|
|
1495
|
+
if (sub) {
|
|
1496
|
+
// sourceConversationItemId for slack messages is `slack-${channelId}-${ts}`
|
|
1497
|
+
// (see line ~372 above); the threadTs we index on is the original
|
|
1498
|
+
// user message ts. originalMessage.ts is exactly that.
|
|
1499
|
+
await sub.markResolvedByThread(originalMessage.ts);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
catch (err) {
|
|
1503
|
+
this.logger.debug('SLA auto-resolve hook failed (non-critical)', {
|
|
1504
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Send error response
|
|
1510
|
+
*
|
|
1511
|
+
* @param message - Original message
|
|
1512
|
+
* @param error - Error that occurred
|
|
1513
|
+
*/
|
|
1514
|
+
async sendErrorResponse(message, error) {
|
|
1515
|
+
await this.slackService.sendMessage({
|
|
1516
|
+
channelId: message.channelId,
|
|
1517
|
+
text: `:warning: Sorry, I encountered an error: ${error.message}`,
|
|
1518
|
+
threadTs: message.threadTs || message.ts,
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Format response for Slack
|
|
1523
|
+
*
|
|
1524
|
+
* Converts markdown to Slack-compatible formatting.
|
|
1525
|
+
*
|
|
1526
|
+
* @param text - Text to format
|
|
1527
|
+
* @returns Formatted text
|
|
1528
|
+
*/
|
|
1529
|
+
formatForSlack(text) {
|
|
1530
|
+
// Convert markdown headers to bold
|
|
1531
|
+
let formatted = text.replace(/^### (.+)$/gm, '*$1*');
|
|
1532
|
+
formatted = formatted.replace(/^## (.+)$/gm, '*$1*');
|
|
1533
|
+
formatted = formatted.replace(/^# (.+)$/gm, '*$1*');
|
|
1534
|
+
// Slack already supports backtick code formatting
|
|
1535
|
+
// Convert code blocks (simplified)
|
|
1536
|
+
formatted = formatted.replace(/```[\w]*\n([\s\S]*?)```/g, '```$1```');
|
|
1537
|
+
return formatted;
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Persist Slack reply content to the thread store when available.
|
|
1541
|
+
*/
|
|
1542
|
+
async recordThreadReply(originalMessage, content) {
|
|
1543
|
+
if (!this.threadStore)
|
|
1544
|
+
return;
|
|
1545
|
+
const trimmed = content?.trim();
|
|
1546
|
+
if (!trimmed)
|
|
1547
|
+
return;
|
|
1548
|
+
const threadTs = originalMessage.threadTs || originalMessage.ts;
|
|
1549
|
+
if (!threadTs)
|
|
1550
|
+
return;
|
|
1551
|
+
try {
|
|
1552
|
+
await this.threadStore.appendOrchestratorReply(originalMessage.channelId, threadTs, trimmed);
|
|
1553
|
+
}
|
|
1554
|
+
catch (err) {
|
|
1555
|
+
this.logger.warn('Failed to store thread reply', { error: err instanceof Error ? err.message : String(err) });
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Send proactive notification to Slack
|
|
1560
|
+
*
|
|
1561
|
+
* @param notification - Notification to send
|
|
1562
|
+
*/
|
|
1563
|
+
async sendNotification(notification) {
|
|
1564
|
+
if (!this.config.enableNotifications)
|
|
1565
|
+
return;
|
|
1566
|
+
await this.slackService.sendNotification(notification);
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Send a content approval notification to Slack with Block Kit buttons.
|
|
1570
|
+
*
|
|
1571
|
+
* Posts a rich message with content preview and approve/reject buttons.
|
|
1572
|
+
* Links the Slack message back to the approval record so interactive
|
|
1573
|
+
* button clicks can resolve the approval.
|
|
1574
|
+
*
|
|
1575
|
+
* @param approvalId - The content approval ID
|
|
1576
|
+
* @param channelId - Slack channel ID
|
|
1577
|
+
* @param threadTs - Optional thread timestamp
|
|
1578
|
+
* @returns The Slack message timestamp, or null if failed
|
|
1579
|
+
*/
|
|
1580
|
+
async sendContentApproval(approvalId, channelId, threadTs) {
|
|
1581
|
+
if (!channelId)
|
|
1582
|
+
return null;
|
|
1583
|
+
try {
|
|
1584
|
+
const approval = ContentApprovalService.getInstance().get(approvalId);
|
|
1585
|
+
const fallbackText = `Content approval requested (ID: ${approvalId.slice(0, 8)}). Use the buttons to approve/reject.`;
|
|
1586
|
+
const blocks = [
|
|
1587
|
+
{
|
|
1588
|
+
type: 'header',
|
|
1589
|
+
text: { type: 'plain_text', text: '\ud83d\udccb Content Approval Required', emoji: true },
|
|
1590
|
+
},
|
|
1591
|
+
{
|
|
1592
|
+
type: 'section',
|
|
1593
|
+
fields: [
|
|
1594
|
+
{ type: 'mrkdwn', text: `*Platform:*\n${approval?.platform ?? 'N/A'}` },
|
|
1595
|
+
{ type: 'mrkdwn', text: `*Type:*\n${approval?.contentType ?? 'N/A'}` },
|
|
1596
|
+
{ type: 'mrkdwn', text: `*Submitted by:*\n${approval?.submittedBy ?? 'Unknown'}` },
|
|
1597
|
+
{ type: 'mrkdwn', text: `*ID:*\n${approvalId.slice(0, 8)}` },
|
|
1598
|
+
],
|
|
1599
|
+
},
|
|
1600
|
+
{
|
|
1601
|
+
type: 'section',
|
|
1602
|
+
text: {
|
|
1603
|
+
type: 'mrkdwn',
|
|
1604
|
+
text: `*Content Preview:*\n${(approval?.content ?? '').slice(0, 300)}${(approval?.content ?? '').length > 300 ? '...' : ''}`,
|
|
1605
|
+
},
|
|
1606
|
+
},
|
|
1607
|
+
];
|
|
1608
|
+
// Add hashtags and scheduled time if present
|
|
1609
|
+
const metaFields = [];
|
|
1610
|
+
if (approval?.hashtags && approval.hashtags.length > 0) {
|
|
1611
|
+
metaFields.push({ type: 'mrkdwn', text: `*Hashtags:*\n${approval.hashtags.join(' ')}` });
|
|
1612
|
+
}
|
|
1613
|
+
if (approval?.scheduledTime) {
|
|
1614
|
+
metaFields.push({ type: 'mrkdwn', text: `*Scheduled:*\n${approval.scheduledTime}` });
|
|
1615
|
+
}
|
|
1616
|
+
if (metaFields.length > 0) {
|
|
1617
|
+
blocks.push({ type: 'section', fields: metaFields });
|
|
1618
|
+
}
|
|
1619
|
+
blocks.push({ type: 'divider' });
|
|
1620
|
+
// Actions block with approve/reject buttons
|
|
1621
|
+
blocks.push({
|
|
1622
|
+
type: 'actions',
|
|
1623
|
+
block_id: `content_approval_${approvalId}`,
|
|
1624
|
+
elements: [
|
|
1625
|
+
{
|
|
1626
|
+
type: 'button',
|
|
1627
|
+
action_id: 'content_approval_approve',
|
|
1628
|
+
text: { type: 'plain_text', text: '\u2705 Approve', emoji: true },
|
|
1629
|
+
value: approvalId,
|
|
1630
|
+
style: 'primary',
|
|
1631
|
+
},
|
|
1632
|
+
{
|
|
1633
|
+
type: 'button',
|
|
1634
|
+
action_id: 'content_approval_reject',
|
|
1635
|
+
text: { type: 'plain_text', text: '\u274c Reject', emoji: true },
|
|
1636
|
+
value: approvalId,
|
|
1637
|
+
style: 'danger',
|
|
1638
|
+
},
|
|
1639
|
+
],
|
|
1640
|
+
});
|
|
1641
|
+
const messageTs = await this.slackService.sendMessage({
|
|
1642
|
+
channelId,
|
|
1643
|
+
text: fallbackText,
|
|
1644
|
+
threadTs,
|
|
1645
|
+
blocks,
|
|
1646
|
+
});
|
|
1647
|
+
// Link the Slack message to the approval for tracking
|
|
1648
|
+
if (messageTs) {
|
|
1649
|
+
ContentApprovalService.getInstance().linkSlackMessage(approvalId, channelId, messageTs);
|
|
1650
|
+
}
|
|
1651
|
+
return messageTs ?? null;
|
|
1652
|
+
}
|
|
1653
|
+
catch (err) {
|
|
1654
|
+
this.logger.warn('Failed to send content approval to Slack', {
|
|
1655
|
+
approvalId: approvalId.slice(0, 8),
|
|
1656
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1657
|
+
});
|
|
1658
|
+
return null;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Notify about task completion
|
|
1663
|
+
*
|
|
1664
|
+
* @param taskTitle - Title of completed task
|
|
1665
|
+
* @param agentName - Name of agent that completed the task
|
|
1666
|
+
* @param projectName - Name of the project
|
|
1667
|
+
*/
|
|
1668
|
+
async notifyTaskCompleted(taskTitle, agentName, projectName) {
|
|
1669
|
+
await this.sendNotification({
|
|
1670
|
+
type: 'task_completed',
|
|
1671
|
+
title: 'Task Completed',
|
|
1672
|
+
message: `*${taskTitle}*\n\nCompleted by ${agentName} on project ${projectName}`,
|
|
1673
|
+
urgency: 'normal',
|
|
1674
|
+
timestamp: new Date().toISOString(),
|
|
1675
|
+
metadata: { projectId: projectName },
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Notify about agent needing assistance
|
|
1680
|
+
*
|
|
1681
|
+
* @param agentName - Name of agent with question
|
|
1682
|
+
* @param question - The question from the agent
|
|
1683
|
+
* @param projectName - Name of the project
|
|
1684
|
+
*/
|
|
1685
|
+
async notifyAgentQuestion(agentName, question, projectName) {
|
|
1686
|
+
await this.sendNotification({
|
|
1687
|
+
type: 'agent_question',
|
|
1688
|
+
title: 'Agent Needs Input',
|
|
1689
|
+
message: `*${agentName}* has a question:\n\n_${question}_`,
|
|
1690
|
+
urgency: 'high',
|
|
1691
|
+
timestamp: new Date().toISOString(),
|
|
1692
|
+
metadata: { agentId: agentName, projectId: projectName },
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Notify about errors
|
|
1697
|
+
*
|
|
1698
|
+
* @param errorMessage - Error message
|
|
1699
|
+
* @param agentName - Optional agent name
|
|
1700
|
+
* @param projectName - Optional project name
|
|
1701
|
+
*/
|
|
1702
|
+
async notifyError(errorMessage, agentName, projectName) {
|
|
1703
|
+
await this.sendNotification({
|
|
1704
|
+
type: 'agent_error',
|
|
1705
|
+
title: 'Error Occurred',
|
|
1706
|
+
message: errorMessage,
|
|
1707
|
+
urgency: 'critical',
|
|
1708
|
+
timestamp: new Date().toISOString(),
|
|
1709
|
+
metadata: { agentId: agentName, projectId: projectName },
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Notify about daily summary
|
|
1714
|
+
*
|
|
1715
|
+
* @param summary - Summary content
|
|
1716
|
+
*/
|
|
1717
|
+
async notifyDailySummary(summary) {
|
|
1718
|
+
await this.sendNotification({
|
|
1719
|
+
type: 'daily_summary',
|
|
1720
|
+
title: 'Daily Summary',
|
|
1721
|
+
message: summary,
|
|
1722
|
+
urgency: 'low',
|
|
1723
|
+
timestamp: new Date().toISOString(),
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Resolve @mention in message text to an agent session (#177).
|
|
1728
|
+
*
|
|
1729
|
+
* Matches patterns: @assistant, @auditor, @sam, @leo, etc.
|
|
1730
|
+
* Looks up the agent name in team configs to find the session name.
|
|
1731
|
+
*
|
|
1732
|
+
* @param text - Message text to scan for @mentions
|
|
1733
|
+
* @returns Target agent info, or null if no mention found
|
|
1734
|
+
*/
|
|
1735
|
+
async resolveMentionTarget(text) {
|
|
1736
|
+
// Match @name at the start of the message
|
|
1737
|
+
const mentionMatch = text.match(/^@(\w+)\s/i);
|
|
1738
|
+
if (!mentionMatch)
|
|
1739
|
+
return null;
|
|
1740
|
+
const mentionedName = mentionMatch[1].toLowerCase();
|
|
1741
|
+
// Built-in aliases
|
|
1742
|
+
const aliases = {
|
|
1743
|
+
assistant: 'crewly-orc-assistant',
|
|
1744
|
+
auditor: 'crewly-auditor',
|
|
1745
|
+
orc: 'crewly-orc',
|
|
1746
|
+
orchestrator: 'crewly-orc',
|
|
1747
|
+
};
|
|
1748
|
+
if (aliases[mentionedName]) {
|
|
1749
|
+
return { name: mentionedName, sessionName: aliases[mentionedName] };
|
|
1750
|
+
}
|
|
1751
|
+
// Look up in team configs
|
|
1752
|
+
try {
|
|
1753
|
+
const StorageService = (await import('../core/storage.service.js')).StorageService;
|
|
1754
|
+
const storage = StorageService.getInstance();
|
|
1755
|
+
const teams = await storage.getTeams();
|
|
1756
|
+
for (const team of teams) {
|
|
1757
|
+
for (const member of (team.members || [])) {
|
|
1758
|
+
// Match by name (case-insensitive) or session name fragment
|
|
1759
|
+
const memberName = member.name?.toLowerCase() || '';
|
|
1760
|
+
const sessionParts = (member.sessionName || '').split('-');
|
|
1761
|
+
const shortName = sessionParts.length >= 3 ? sessionParts[2] : '';
|
|
1762
|
+
if (memberName === mentionedName || shortName === mentionedName) {
|
|
1763
|
+
return { name: mentionedName, sessionName: member.sessionName };
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
catch (err) {
|
|
1769
|
+
this.logger.warn('Failed to look up mention target', {
|
|
1770
|
+
mention: mentionedName,
|
|
1771
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
return null;
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* Send a message to a specific agent session via terminal deliver (#177).
|
|
1778
|
+
*
|
|
1779
|
+
* @param sessionName - Target agent session
|
|
1780
|
+
* @param message - Message to send
|
|
1781
|
+
* @param context - Slack context for response routing
|
|
1782
|
+
* @returns OrcResponse envelope — `fromOrcReply` is `true` ONLY when the
|
|
1783
|
+
* reply came from the agent's `slackResolve` callback (a real reply
|
|
1784
|
+
* via the reply-slack skill). Timeout / direct-delivery / error
|
|
1785
|
+
* placeholders return `fromOrcReply: false` so callers can gate the
|
|
1786
|
+
* SLA cascade. Issue #394.
|
|
1787
|
+
*/
|
|
1788
|
+
async sendToAgent(sessionName, message, context) {
|
|
1789
|
+
try {
|
|
1790
|
+
// Enrich with Slack context for reply routing
|
|
1791
|
+
let enrichedMessage = message;
|
|
1792
|
+
if (context) {
|
|
1793
|
+
enrichedMessage = `[SLACK_CONTEXT:channelId=${context.channelId},threadTs=${context.threadTs || ''}]\n${message}`;
|
|
1794
|
+
}
|
|
1795
|
+
// Use message queue for agents that support it, or deliver directly
|
|
1796
|
+
if (this.messageQueueService) {
|
|
1797
|
+
const chatResult = {
|
|
1798
|
+
conversation: {
|
|
1799
|
+
id: this.persistSlackInbound({
|
|
1800
|
+
content: enrichedMessage,
|
|
1801
|
+
conversationId: context?.conversationId,
|
|
1802
|
+
agentSession: sessionName,
|
|
1803
|
+
channelId: context?.channelId,
|
|
1804
|
+
threadTs: context?.threadTs,
|
|
1805
|
+
userId: context?.userId,
|
|
1806
|
+
}).conversationId,
|
|
1807
|
+
},
|
|
1808
|
+
};
|
|
1809
|
+
return new Promise((resolve) => {
|
|
1810
|
+
let resolved = false;
|
|
1811
|
+
const timeoutId = setTimeout(() => {
|
|
1812
|
+
if (!resolved) {
|
|
1813
|
+
resolved = true;
|
|
1814
|
+
// Timeout placeholder — explicitly NOT a real reply.
|
|
1815
|
+
resolve({
|
|
1816
|
+
response: 'Agent is processing your message. Check back shortly.',
|
|
1817
|
+
fromOrcReply: false,
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
}, this.config.responseTimeoutMs);
|
|
1821
|
+
this.messageQueueService.enqueue({
|
|
1822
|
+
content: enrichedMessage,
|
|
1823
|
+
conversationId: chatResult.conversation.id,
|
|
1824
|
+
source: 'slack',
|
|
1825
|
+
targetSession: sessionName,
|
|
1826
|
+
sourceMetadata: {
|
|
1827
|
+
slackResolve: (resp) => {
|
|
1828
|
+
if (!resolved) {
|
|
1829
|
+
resolved = true;
|
|
1830
|
+
clearTimeout(timeoutId);
|
|
1831
|
+
// Real agent reply path. Empty `resp` is the
|
|
1832
|
+
// QueueProcessor fire-and-forget delivery ack — treat
|
|
1833
|
+
// it as a non-reply too (mirrors the orchestrator
|
|
1834
|
+
// path's empty-string gate in sendToOrchestrator).
|
|
1835
|
+
resolve({ response: resp, fromOrcReply: resp.length > 0 });
|
|
1836
|
+
}
|
|
1837
|
+
},
|
|
1838
|
+
channelId: context?.channelId,
|
|
1839
|
+
threadTs: context?.threadTs,
|
|
1840
|
+
},
|
|
1841
|
+
});
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
// Fallback: direct terminal delivery — the agent acknowledges the
|
|
1845
|
+
// write, but the actual reply hasn't been generated yet. Return a
|
|
1846
|
+
// placeholder with fromOrcReply=false so the SLA cascade does NOT
|
|
1847
|
+
// close on this.
|
|
1848
|
+
const { default: fetch } = await import('node-fetch').catch(() => ({ default: globalThis.fetch }));
|
|
1849
|
+
const apiUrl = process.env.CREWLY_API_URL || 'http://localhost:8787';
|
|
1850
|
+
await fetch(`${apiUrl}/api/terminal/${sessionName}/deliver`, {
|
|
1851
|
+
method: 'POST',
|
|
1852
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1853
|
+
body: JSON.stringify({ message: enrichedMessage, force: true }),
|
|
1854
|
+
});
|
|
1855
|
+
return {
|
|
1856
|
+
response: 'Message delivered to agent. Response will arrive shortly.',
|
|
1857
|
+
fromOrcReply: false,
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
catch (err) {
|
|
1861
|
+
this.logger.error('Failed to send to agent', {
|
|
1862
|
+
session: sessionName,
|
|
1863
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1864
|
+
});
|
|
1865
|
+
return {
|
|
1866
|
+
response: `Failed to reach agent. Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
1867
|
+
fromOrcReply: false,
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* Persist a Slack inbound user message to chat-v2 and return the
|
|
1873
|
+
* `(conversationId, messageId)` pair callers thread downstream
|
|
1874
|
+
* into `MessageQueueService.enqueue` and `ThreadStatusQueueService.trackInbound`.
|
|
1875
|
+
*
|
|
1876
|
+
* Phase 6c of unified-chat-message-store spec: replaces the
|
|
1877
|
+
* legacy `chatService.sendMessage` call sites on this bridge.
|
|
1878
|
+
* Synthesizes a stable conversationId from Slack channel + thread
|
|
1879
|
+
* ts when the caller didn't supply one, so re-enqueue paths
|
|
1880
|
+
* (offline replay, recovery) reuse the same channel.
|
|
1881
|
+
*
|
|
1882
|
+
* @param args - Persistence inputs (content + slack context + recipient agent)
|
|
1883
|
+
* @returns The chat-v2 channelId and the freshly persisted messageId
|
|
1884
|
+
*/
|
|
1885
|
+
persistSlackInbound(args) {
|
|
1886
|
+
const cid = args.conversationId ??
|
|
1887
|
+
synthesizeSlackConversationId(args.channelId ?? 'unknown', args.threadTs ?? String(Date.now()));
|
|
1888
|
+
const channel = this.chatV2.ensureChannelForLegacyConversation({
|
|
1889
|
+
conversationId: cid,
|
|
1890
|
+
agentSession: args.agentSession,
|
|
1891
|
+
// Name the thread by its first message so the consolidated chat shows a
|
|
1892
|
+
// readable title (e.g. "Slack: ship the v2 launch…") instead of N
|
|
1893
|
+
// identical synthesized ids. ensure… only sets the name on create, so
|
|
1894
|
+
// this stamps the thread's opening message. Falls back to the id.
|
|
1895
|
+
name: deriveSlackThreadName(args.content),
|
|
1896
|
+
});
|
|
1897
|
+
const { message } = this.chatV2.recordTurn({
|
|
1898
|
+
channelId: channel.id,
|
|
1899
|
+
senderType: 'user',
|
|
1900
|
+
senderId: args.userId ?? 'slack-user',
|
|
1901
|
+
content: args.content,
|
|
1902
|
+
metadata: {
|
|
1903
|
+
source: 'slack',
|
|
1904
|
+
slackChannelId: args.channelId,
|
|
1905
|
+
slackThreadTs: args.threadTs,
|
|
1906
|
+
},
|
|
1907
|
+
});
|
|
1908
|
+
return { conversationId: channel.id, messageId: message.id };
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Get bridge singleton
|
|
1913
|
+
*
|
|
1914
|
+
* @returns SlackOrchestratorBridge instance
|
|
1915
|
+
*/
|
|
1916
|
+
export function getSlackOrchestratorBridge() {
|
|
1917
|
+
if (!bridgeInstance) {
|
|
1918
|
+
bridgeInstance = new SlackOrchestratorBridge();
|
|
1919
|
+
}
|
|
1920
|
+
return bridgeInstance;
|
|
1921
|
+
}
|
|
1922
|
+
/**
|
|
1923
|
+
* Reset bridge (for testing)
|
|
1924
|
+
*/
|
|
1925
|
+
export function resetSlackOrchestratorBridge() {
|
|
1926
|
+
bridgeInstance = null;
|
|
1927
|
+
}
|
|
1928
|
+
//# sourceMappingURL=slack-orchestrator-bridge.js.map
|