@polpo-ai/node 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +13 -0
- package/dist/adapters/engine.d.ts +51 -0
- package/dist/adapters/engine.d.ts.map +1 -0
- package/dist/adapters/engine.js +316 -0
- package/dist/adapters/engine.js.map +1 -0
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +4 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/loop-engine.d.ts +36 -0
- package/dist/adapters/loop-engine.d.ts.map +1 -0
- package/dist/adapters/loop-engine.js +534 -0
- package/dist/adapters/loop-engine.js.map +1 -0
- package/dist/adapters/node-filesystem.d.ts +22 -0
- package/dist/adapters/node-filesystem.d.ts.map +1 -0
- package/dist/adapters/node-filesystem.js +59 -0
- package/dist/adapters/node-filesystem.js.map +1 -0
- package/dist/adapters/node-shell.d.ts +5 -0
- package/dist/adapters/node-shell.d.ts.map +1 -0
- package/dist/adapters/node-shell.js +34 -0
- package/dist/adapters/node-shell.js.map +1 -0
- package/dist/adapters/node-spawner.d.ts +14 -0
- package/dist/adapters/node-spawner.d.ts.map +1 -0
- package/dist/adapters/node-spawner.js +103 -0
- package/dist/adapters/node-spawner.js.map +1 -0
- package/dist/assessment/assessor.d.ts +6 -0
- package/dist/assessment/assessor.d.ts.map +1 -0
- package/dist/assessment/assessor.js +37 -0
- package/dist/assessment/assessor.js.map +1 -0
- package/dist/assessment/index.d.ts +6 -0
- package/dist/assessment/index.d.ts.map +1 -0
- package/dist/assessment/index.js +6 -0
- package/dist/assessment/index.js.map +1 -0
- package/dist/assessment/llm-review.d.ts +34 -0
- package/dist/assessment/llm-review.d.ts.map +1 -0
- package/dist/assessment/llm-review.js +669 -0
- package/dist/assessment/llm-review.js.map +1 -0
- package/dist/assessment/schemas.d.ts +5 -0
- package/dist/assessment/schemas.d.ts.map +1 -0
- package/dist/assessment/schemas.js +5 -0
- package/dist/assessment/schemas.js.map +1 -0
- package/dist/assessment/scoring.d.ts +5 -0
- package/dist/assessment/scoring.d.ts.map +1 -0
- package/dist/assessment/scoring.js +5 -0
- package/dist/assessment/scoring.js.map +1 -0
- package/dist/assessment/transcript-parser.d.ts +31 -0
- package/dist/assessment/transcript-parser.d.ts.map +1 -0
- package/dist/assessment/transcript-parser.js +203 -0
- package/dist/assessment/transcript-parser.js.map +1 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +4 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/core/adapter.d.ts +62 -0
- package/dist/core/adapter.d.ts.map +1 -0
- package/dist/core/adapter.js +2 -0
- package/dist/core/adapter.js.map +1 -0
- package/dist/core/assessment-orchestrator.d.ts +18 -0
- package/dist/core/assessment-orchestrator.d.ts.map +1 -0
- package/dist/core/assessment-orchestrator.js +93 -0
- package/dist/core/assessment-orchestrator.js.map +1 -0
- package/dist/core/config.d.ts +17 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +272 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/constants.d.ts +10 -0
- package/dist/core/constants.d.ts.map +1 -0
- package/dist/core/constants.js +11 -0
- package/dist/core/constants.js.map +1 -0
- package/dist/core/deadlock-resolver.d.ts +35 -0
- package/dist/core/deadlock-resolver.d.ts.map +1 -0
- package/dist/core/deadlock-resolver.js +317 -0
- package/dist/core/deadlock-resolver.js.map +1 -0
- package/dist/core/drizzle-sqlite-schema.d.ts +8 -0
- package/dist/core/drizzle-sqlite-schema.d.ts.map +1 -0
- package/dist/core/drizzle-sqlite-schema.js +250 -0
- package/dist/core/drizzle-sqlite-schema.js.map +1 -0
- package/dist/core/events.d.ts +26 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/events.js +39 -0
- package/dist/core/events.js.map +1 -0
- package/dist/core/index.d.ts +37 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +31 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/ink.d.ts +165 -0
- package/dist/core/ink.d.ts.map +1 -0
- package/dist/core/ink.js +445 -0
- package/dist/core/ink.js.map +1 -0
- package/dist/core/orchestrator.d.ts +387 -0
- package/dist/core/orchestrator.d.ts.map +1 -0
- package/dist/core/orchestrator.js +1015 -0
- package/dist/core/orchestrator.js.map +1 -0
- package/dist/core/question-detector.d.ts +19 -0
- package/dist/core/question-detector.d.ts.map +1 -0
- package/dist/core/question-detector.js +29 -0
- package/dist/core/question-detector.js.map +1 -0
- package/dist/core/runner.d.ts +14 -0
- package/dist/core/runner.d.ts.map +1 -0
- package/dist/core/runner.js +299 -0
- package/dist/core/runner.js.map +1 -0
- package/dist/core/session-reader.d.ts +46 -0
- package/dist/core/session-reader.d.ts.map +1 -0
- package/dist/core/session-reader.js +166 -0
- package/dist/core/session-reader.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/index.d.ts +8 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +21 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/llm/pi-client.d.ts +8 -0
- package/dist/llm/pi-client.d.ts.map +1 -0
- package/dist/llm/pi-client.js +8 -0
- package/dist/llm/pi-client.js.map +1 -0
- package/dist/llm/prompts.d.ts +14 -0
- package/dist/llm/prompts.d.ts.map +1 -0
- package/dist/llm/prompts.js +1618 -0
- package/dist/llm/prompts.js.map +1 -0
- package/dist/llm/retry.d.ts +5 -0
- package/dist/llm/retry.d.ts.map +1 -0
- package/dist/llm/retry.js +5 -0
- package/dist/llm/retry.js.map +1 -0
- package/dist/llm/skills.d.ts +265 -0
- package/dist/llm/skills.d.ts.map +1 -0
- package/dist/llm/skills.js +804 -0
- package/dist/llm/skills.js.map +1 -0
- package/dist/quality/index.d.ts +3 -0
- package/dist/quality/index.d.ts.map +1 -0
- package/dist/quality/index.js +3 -0
- package/dist/quality/index.js.map +1 -0
- package/dist/quality/quality-controller.d.ts +2 -0
- package/dist/quality/quality-controller.d.ts.map +1 -0
- package/dist/quality/quality-controller.js +2 -0
- package/dist/quality/quality-controller.js.map +1 -0
- package/dist/quality/sla-monitor.d.ts +2 -0
- package/dist/quality/sla-monitor.d.ts.map +1 -0
- package/dist/quality/sla-monitor.js +2 -0
- package/dist/quality/sla-monitor.js.map +1 -0
- package/dist/scheduling/cron.d.ts +2 -0
- package/dist/scheduling/cron.d.ts.map +1 -0
- package/dist/scheduling/cron.js +2 -0
- package/dist/scheduling/cron.js.map +1 -0
- package/dist/scheduling/index.d.ts +3 -0
- package/dist/scheduling/index.d.ts.map +1 -0
- package/dist/scheduling/index.js +3 -0
- package/dist/scheduling/index.js.map +1 -0
- package/dist/scheduling/scheduler.d.ts +2 -0
- package/dist/scheduling/scheduler.d.ts.map +1 -0
- package/dist/scheduling/scheduler.js +2 -0
- package/dist/scheduling/scheduler.js.map +1 -0
- package/dist/server/app.d.ts +19 -0
- package/dist/server/app.d.ts.map +1 -0
- package/dist/server/app.js +302 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/deps.d.ts +157 -0
- package/dist/server/deps.d.ts.map +1 -0
- package/dist/server/deps.js +9 -0
- package/dist/server/deps.js.map +1 -0
- package/dist/server/index.d.ts +43 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +128 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/middleware/auth.d.ts +3 -0
- package/dist/server/middleware/auth.d.ts.map +1 -0
- package/dist/server/middleware/auth.js +35 -0
- package/dist/server/middleware/auth.js.map +1 -0
- package/dist/server/middleware/error.d.ts +15 -0
- package/dist/server/middleware/error.d.ts.map +1 -0
- package/dist/server/middleware/error.js +44 -0
- package/dist/server/middleware/error.js.map +1 -0
- package/dist/server/middleware/rate-limit.d.ts +16 -0
- package/dist/server/middleware/rate-limit.d.ts.map +1 -0
- package/dist/server/middleware/rate-limit.js +51 -0
- package/dist/server/middleware/rate-limit.js.map +1 -0
- package/dist/server/routes/config.d.ts +25 -0
- package/dist/server/routes/config.d.ts.map +1 -0
- package/dist/server/routes/config.js +446 -0
- package/dist/server/routes/config.js.map +1 -0
- package/dist/server/routes/files.d.ts +11 -0
- package/dist/server/routes/files.d.ts.map +1 -0
- package/dist/server/routes/files.js +514 -0
- package/dist/server/routes/files.js.map +1 -0
- package/dist/server/routes/filesystem.d.ts +3 -0
- package/dist/server/routes/filesystem.d.ts.map +1 -0
- package/dist/server/routes/filesystem.js +152 -0
- package/dist/server/routes/filesystem.js.map +1 -0
- package/dist/server/routes/providers.d.ts +7 -0
- package/dist/server/routes/providers.d.ts.map +1 -0
- package/dist/server/routes/providers.js +256 -0
- package/dist/server/routes/providers.js.map +1 -0
- package/dist/server/routes/skills.d.ts +11 -0
- package/dist/server/routes/skills.d.ts.map +1 -0
- package/dist/server/routes/skills.js +543 -0
- package/dist/server/routes/skills.js.map +1 -0
- package/dist/server/security.d.ts +34 -0
- package/dist/server/security.d.ts.map +1 -0
- package/dist/server/security.js +118 -0
- package/dist/server/security.js.map +1 -0
- package/dist/server/sse-bridge.d.ts +36 -0
- package/dist/server/sse-bridge.d.ts.map +1 -0
- package/dist/server/sse-bridge.js +110 -0
- package/dist/server/sse-bridge.js.map +1 -0
- package/dist/server/types.d.ts +69 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +2 -0
- package/dist/server/types.js.map +1 -0
- package/dist/setup/env-persistence.d.ts +9 -0
- package/dist/setup/env-persistence.d.ts.map +1 -0
- package/dist/setup/env-persistence.js +42 -0
- package/dist/setup/env-persistence.js.map +1 -0
- package/dist/setup/index.d.ts +32 -0
- package/dist/setup/index.d.ts.map +1 -0
- package/dist/setup/index.js +24 -0
- package/dist/setup/index.js.map +1 -0
- package/dist/setup/models.d.ts +20 -0
- package/dist/setup/models.d.ts.map +1 -0
- package/dist/setup/models.js +33 -0
- package/dist/setup/models.js.map +1 -0
- package/dist/setup/providers.d.ts +26 -0
- package/dist/setup/providers.d.ts.map +1 -0
- package/dist/setup/providers.js +31 -0
- package/dist/setup/providers.js.map +1 -0
- package/dist/vault/encrypted-store.d.ts +45 -0
- package/dist/vault/encrypted-store.d.ts.map +1 -0
- package/dist/vault/encrypted-store.js +149 -0
- package/dist/vault/encrypted-store.js.map +1 -0
- package/dist/vault/index.d.ts +4 -0
- package/dist/vault/index.d.ts.map +1 -0
- package/dist/vault/index.js +3 -0
- package/dist/vault/index.js.map +1 -0
- package/dist/vault/resolver.d.ts +8 -0
- package/dist/vault/resolver.d.ts.map +1 -0
- package/dist/vault/resolver.js +8 -0
- package/dist/vault/resolver.js.map +1 -0
- package/package.json +76 -0
|
@@ -0,0 +1,1015 @@
|
|
|
1
|
+
import { resolve, join } from "node:path";
|
|
2
|
+
import { mkdirSync, existsSync, readFileSync, watch } from "node:fs";
|
|
3
|
+
import { getPolpoDir } from "./constants.js";
|
|
4
|
+
import { parseConfig, loadPolpoConfig, savePolpoConfig, loadEnvFile } from "./config.js";
|
|
5
|
+
import { findLogForTask, buildExecutionSummary } from "../assessment/transcript-parser.js";
|
|
6
|
+
import { FileTaskStore } from "@polpo-ai/file-stores";
|
|
7
|
+
import { FileRunStore } from "@polpo-ai/file-stores";
|
|
8
|
+
import { FileMemoryStore } from "@polpo-ai/file-stores";
|
|
9
|
+
import { FileLogStore } from "@polpo-ai/file-stores";
|
|
10
|
+
import { FileSessionStore } from "@polpo-ai/file-stores";
|
|
11
|
+
import { assessTask } from "../assessment/assessor.js";
|
|
12
|
+
import { analyzeBlockedTasks, resolveDeadlock, isResolving } from "./deadlock-resolver.js";
|
|
13
|
+
import { OrchestratorEngine, resolveMissionStore } from "@polpo-ai/core";
|
|
14
|
+
import { TypedEmitter } from "./events.js";
|
|
15
|
+
import { AgentManager } from "@polpo-ai/core/agent-manager";
|
|
16
|
+
import { TaskManager } from "@polpo-ai/core/task-manager";
|
|
17
|
+
import { MissionExecutor } from "@polpo-ai/core/mission-executor";
|
|
18
|
+
import { TaskRunner } from "@polpo-ai/core";
|
|
19
|
+
import { AssessmentOrchestrator } from "./assessment-orchestrator.js";
|
|
20
|
+
import { buildFixPrompt, buildRetryPrompt, sleep, } from "@polpo-ai/core/assessment-prompts";
|
|
21
|
+
import { setProviderOverrides, validateProviderKeys, setModelAllowlist } from "../llm/pi-client.js";
|
|
22
|
+
import { HookRegistry } from "@polpo-ai/core/hooks";
|
|
23
|
+
import { ApprovalManager } from "@polpo-ai/core/approval-manager";
|
|
24
|
+
import { FileApprovalStore } from "@polpo-ai/file-stores";
|
|
25
|
+
import { FileTeamStore } from "@polpo-ai/file-stores";
|
|
26
|
+
import { FileAgentStore } from "@polpo-ai/file-stores";
|
|
27
|
+
import { EscalationManager } from "@polpo-ai/core/escalation-manager";
|
|
28
|
+
import { SLAMonitor } from "../quality/sla-monitor.js";
|
|
29
|
+
import { QualityController } from "../quality/quality-controller.js";
|
|
30
|
+
import { Scheduler } from "../scheduling/scheduler.js";
|
|
31
|
+
import { TaskWatcherManager } from "@polpo-ai/core/task-watcher";
|
|
32
|
+
import { EncryptedVaultStore } from "../vault/encrypted-store.js";
|
|
33
|
+
import { FilePlaybookStore } from "@polpo-ai/file-stores";
|
|
34
|
+
import { NodeSpawner } from "../adapters/node-spawner.js";
|
|
35
|
+
import { NodeFileSystem } from "../adapters/node-filesystem.js";
|
|
36
|
+
import { NodeShell } from "../adapters/node-shell.js";
|
|
37
|
+
// Re-export for backward compatibility (consumed by core/index.ts and external modules)
|
|
38
|
+
export { buildFixPrompt, buildRetryPrompt };
|
|
39
|
+
export class Orchestrator extends TypedEmitter {
|
|
40
|
+
registry;
|
|
41
|
+
runStore;
|
|
42
|
+
config;
|
|
43
|
+
polpoDir;
|
|
44
|
+
workDir;
|
|
45
|
+
/** Cached resolved agent working directory (invalidated on config reload). */
|
|
46
|
+
cachedAgentWorkDir = null;
|
|
47
|
+
interactive = false;
|
|
48
|
+
stopped = false;
|
|
49
|
+
assessFn;
|
|
50
|
+
spawner;
|
|
51
|
+
injectedStore;
|
|
52
|
+
injectedRunStore;
|
|
53
|
+
memoryStore;
|
|
54
|
+
logStore;
|
|
55
|
+
sessionStore;
|
|
56
|
+
hookRegistry = new HookRegistry();
|
|
57
|
+
approvalMgr;
|
|
58
|
+
escalationMgr;
|
|
59
|
+
slaMonitor;
|
|
60
|
+
qualityController;
|
|
61
|
+
scheduler;
|
|
62
|
+
watcherMgr;
|
|
63
|
+
teamStore;
|
|
64
|
+
agentStore;
|
|
65
|
+
configWatcher;
|
|
66
|
+
configReloadTimer;
|
|
67
|
+
vaultStore;
|
|
68
|
+
playbookStore;
|
|
69
|
+
fs;
|
|
70
|
+
shell;
|
|
71
|
+
gatewayConfig;
|
|
72
|
+
// Managers
|
|
73
|
+
agentMgr;
|
|
74
|
+
taskMgr;
|
|
75
|
+
missionExec;
|
|
76
|
+
runner;
|
|
77
|
+
assessor;
|
|
78
|
+
// Pure orchestration engine (delegates tick, run, and all pure-logic methods)
|
|
79
|
+
engine;
|
|
80
|
+
getWorkDir() { return this.workDir; }
|
|
81
|
+
getAgentWorkDir() {
|
|
82
|
+
if (!this.cachedAgentWorkDir) {
|
|
83
|
+
this.cachedAgentWorkDir = this.resolveAgentWorkDir();
|
|
84
|
+
}
|
|
85
|
+
return this.cachedAgentWorkDir;
|
|
86
|
+
}
|
|
87
|
+
getHooks() { return this.hookRegistry; }
|
|
88
|
+
getSLAMonitor() { return this.slaMonitor; }
|
|
89
|
+
getQualityController() { return this.qualityController; }
|
|
90
|
+
getScheduler() { return this.scheduler; }
|
|
91
|
+
getWatcherManager() { return this.watcherMgr; }
|
|
92
|
+
/** Re-point the orchestrator at a different project directory (before init). */
|
|
93
|
+
resetWorkDir(newWorkDir) {
|
|
94
|
+
this.workDir = resolve(newWorkDir);
|
|
95
|
+
this.polpoDir = getPolpoDir(this.workDir);
|
|
96
|
+
this.cachedAgentWorkDir = null;
|
|
97
|
+
}
|
|
98
|
+
constructor(workDirOrOptions) {
|
|
99
|
+
super();
|
|
100
|
+
// Centralized fs/shell creation — the ONE place that creates Node.js defaults.
|
|
101
|
+
// Everything downstream receives these via getters or SpawnContext.
|
|
102
|
+
this.fs = new NodeFileSystem();
|
|
103
|
+
this.shell = new NodeShell();
|
|
104
|
+
if (typeof workDirOrOptions === "string" || workDirOrOptions === undefined) {
|
|
105
|
+
const workDir = workDirOrOptions ?? ".";
|
|
106
|
+
this.workDir = resolve(workDir);
|
|
107
|
+
this.polpoDir = getPolpoDir(this.workDir);
|
|
108
|
+
this.assessFn = assessTask;
|
|
109
|
+
this.spawner = new NodeSpawner({ polpoDir: this.polpoDir, cwd: this.workDir });
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const opts = workDirOrOptions;
|
|
113
|
+
this.workDir = resolve(opts.workDir ?? ".");
|
|
114
|
+
this.polpoDir = getPolpoDir(this.workDir);
|
|
115
|
+
this.assessFn = opts.assessFn ?? assessTask;
|
|
116
|
+
this.injectedStore = opts.store;
|
|
117
|
+
this.injectedRunStore = opts.runStore;
|
|
118
|
+
this.spawner = opts.spawner ?? new NodeSpawner({ polpoDir: this.polpoDir, cwd: this.workDir });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Drizzle store bundle — populated when storage is "sqlite" or "postgres". */
|
|
122
|
+
drizzleStores;
|
|
123
|
+
/** Create task + run stores based on the configured storage backend. */
|
|
124
|
+
async createStores(storage, databaseUrl) {
|
|
125
|
+
if (storage === "postgres") {
|
|
126
|
+
const dbUrl = databaseUrl ?? this.config?.settings?.databaseUrl;
|
|
127
|
+
if (!dbUrl)
|
|
128
|
+
throw new Error('storage: "postgres" requires a databaseUrl');
|
|
129
|
+
const { createPgStores, ensurePgSchema } = await import("@polpo-ai/drizzle");
|
|
130
|
+
const postgres = (await import("postgres")).default;
|
|
131
|
+
const { drizzle } = await import("drizzle-orm/postgres-js");
|
|
132
|
+
const sql = postgres(dbUrl);
|
|
133
|
+
const db = drizzle(sql);
|
|
134
|
+
await ensurePgSchema(db);
|
|
135
|
+
this.drizzleStores = createPgStores(db);
|
|
136
|
+
return {
|
|
137
|
+
task: this.drizzleStores.taskStore,
|
|
138
|
+
run: this.drizzleStores.runStore,
|
|
139
|
+
logStore: this.drizzleStores.logStore,
|
|
140
|
+
sessionStore: this.drizzleStores.sessionStore,
|
|
141
|
+
memoryStore: this.drizzleStores.memoryStore,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (storage === "sqlite") {
|
|
145
|
+
const { createSqliteStores } = await import("@polpo-ai/drizzle");
|
|
146
|
+
const { createRequire } = await import("node:module");
|
|
147
|
+
const req = createRequire(import.meta.url);
|
|
148
|
+
const Database = req("better-sqlite3");
|
|
149
|
+
const dbPath = join(this.polpoDir, "state.db");
|
|
150
|
+
const sqlite = new Database(dbPath);
|
|
151
|
+
sqlite.exec("PRAGMA journal_mode = WAL");
|
|
152
|
+
sqlite.exec("PRAGMA synchronous = NORMAL");
|
|
153
|
+
sqlite.exec("PRAGMA foreign_keys = ON");
|
|
154
|
+
const { ensureSqliteSchema } = await import("./drizzle-sqlite-schema.js");
|
|
155
|
+
ensureSqliteSchema(sqlite);
|
|
156
|
+
const { drizzle } = await import("drizzle-orm/better-sqlite3");
|
|
157
|
+
const db = drizzle(sqlite);
|
|
158
|
+
this.drizzleStores = createSqliteStores(db);
|
|
159
|
+
return {
|
|
160
|
+
task: this.drizzleStores.taskStore,
|
|
161
|
+
run: this.drizzleStores.runStore,
|
|
162
|
+
logStore: this.drizzleStores.logStore,
|
|
163
|
+
sessionStore: this.drizzleStores.sessionStore,
|
|
164
|
+
memoryStore: this.drizzleStores.memoryStore,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
task: new FileTaskStore(this.polpoDir),
|
|
169
|
+
run: new FileRunStore(this.polpoDir),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
async init() {
|
|
173
|
+
this.config = await parseConfig(this.workDir);
|
|
174
|
+
// Apply provider overrides from config
|
|
175
|
+
if (this.config.providers) {
|
|
176
|
+
setProviderOverrides(this.config.providers);
|
|
177
|
+
}
|
|
178
|
+
// Apply model allowlist from settings
|
|
179
|
+
if (this.config.settings.modelAllowlist) {
|
|
180
|
+
setModelAllowlist(this.config.settings.modelAllowlist);
|
|
181
|
+
}
|
|
182
|
+
// Configure LLM gateway from settings
|
|
183
|
+
const gatewaySettings = this.config.settings.gateway;
|
|
184
|
+
if (gatewaySettings) {
|
|
185
|
+
this.gatewayConfig = {
|
|
186
|
+
url: gatewaySettings.url,
|
|
187
|
+
apiKey: gatewaySettings.apiKeyEnv ? process.env[gatewaySettings.apiKeyEnv] : undefined,
|
|
188
|
+
headers: gatewaySettings.headers,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const stores = this.injectedStore
|
|
192
|
+
? { task: this.injectedStore, run: this.injectedRunStore }
|
|
193
|
+
: await this.createStores(this.config.settings.storage, this.config.settings.databaseUrl);
|
|
194
|
+
this.registry = stores.task;
|
|
195
|
+
this.runStore = stores.run;
|
|
196
|
+
// When storage is "postgres", Drizzle provides all stores; otherwise use file-based defaults
|
|
197
|
+
if ("logStore" in stores && stores.logStore) {
|
|
198
|
+
this.logStore = stores.logStore;
|
|
199
|
+
await this.logStore.startSession();
|
|
200
|
+
this.setLogSink(this.logStore);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
await this.initLogStore();
|
|
204
|
+
}
|
|
205
|
+
if ("sessionStore" in stores && stores.sessionStore) {
|
|
206
|
+
this.sessionStore = stores.sessionStore;
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
await this.initSessionStore();
|
|
210
|
+
}
|
|
211
|
+
this.memoryStore = ("memoryStore" in stores && stores.memoryStore)
|
|
212
|
+
? stores.memoryStore
|
|
213
|
+
: new FileMemoryStore(this.polpoDir);
|
|
214
|
+
// Team & Agent stores — Drizzle when available, otherwise file-based
|
|
215
|
+
this.teamStore = this.drizzleStores?.teamStore ?? new FileTeamStore(this.polpoDir);
|
|
216
|
+
this.agentStore = this.drizzleStores?.agentStore ?? new FileAgentStore(this.polpoDir);
|
|
217
|
+
// Validate API keys (after stores are available so we can read per-agent models)
|
|
218
|
+
await this.validateProviders();
|
|
219
|
+
await this.initManagers();
|
|
220
|
+
// Sync config.teams from stores (authoritative source — agents.json / teams.json)
|
|
221
|
+
await this.agentMgr.syncConfigCache();
|
|
222
|
+
this.initVaultStore();
|
|
223
|
+
this.playbookStore = this.drizzleStores?.playbookStore ?? new FilePlaybookStore(this.workDir, this.polpoDir);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Populate stores from the teams array passed to initInteractive().
|
|
227
|
+
* Idempotent — skips teams/agents that already exist in the store.
|
|
228
|
+
*/
|
|
229
|
+
async populateStores(teams) {
|
|
230
|
+
if (!teams || teams.length === 0)
|
|
231
|
+
return;
|
|
232
|
+
await this.teamStore.seed(teams);
|
|
233
|
+
const agentsToSeed = [];
|
|
234
|
+
for (const team of teams) {
|
|
235
|
+
for (const agent of team.agents) {
|
|
236
|
+
agentsToSeed.push({ ...agent, teamName: team.name });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (agentsToSeed.length > 0) {
|
|
240
|
+
await this.agentStore.seed(agentsToSeed);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async validateProviders() {
|
|
244
|
+
const modelSpecs = [];
|
|
245
|
+
// Default model
|
|
246
|
+
if (process.env.POLPO_MODEL)
|
|
247
|
+
modelSpecs.push(process.env.POLPO_MODEL);
|
|
248
|
+
// Orchestrator model
|
|
249
|
+
if (this.config.settings.orchestratorModel) {
|
|
250
|
+
const om = this.config.settings.orchestratorModel;
|
|
251
|
+
if (typeof om === "string") {
|
|
252
|
+
modelSpecs.push(om);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
if (om.primary)
|
|
256
|
+
modelSpecs.push(om.primary);
|
|
257
|
+
if (om.fallbacks)
|
|
258
|
+
modelSpecs.push(...om.fallbacks);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Judge model
|
|
262
|
+
if (process.env.POLPO_JUDGE_MODEL)
|
|
263
|
+
modelSpecs.push(process.env.POLPO_JUDGE_MODEL);
|
|
264
|
+
// Per-agent models (from AgentStore, not config)
|
|
265
|
+
const agents = await this.agentStore.getAgents();
|
|
266
|
+
for (const agent of agents) {
|
|
267
|
+
if (agent.model)
|
|
268
|
+
modelSpecs.push(agent.model);
|
|
269
|
+
}
|
|
270
|
+
if (modelSpecs.length === 0) {
|
|
271
|
+
this.emit("log", {
|
|
272
|
+
level: "warn",
|
|
273
|
+
message: "No model configured for any agent. Agent spawning will fail. Set a model in .polpo/polpo.json (`settings.orchestratorModel`) or on the agent (`model` field), and set the provider API key env var.",
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const missing = validateProviderKeys(modelSpecs);
|
|
278
|
+
if (missing.length > 0) {
|
|
279
|
+
const details = missing
|
|
280
|
+
.map(m => ` - ${m.provider} (model: ${m.modelSpec})`)
|
|
281
|
+
.join("\n");
|
|
282
|
+
this.emit("log", {
|
|
283
|
+
level: "warn",
|
|
284
|
+
message: `Missing API keys for providers:\n${details}\nSet the corresponding environment variables in your shell or .polpo/.env.`,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/** Resolve the directory where agent processes will run (settings.workDir relative to project root). */
|
|
289
|
+
resolveAgentWorkDir() {
|
|
290
|
+
const settingsWorkDir = this.config.settings.workDir;
|
|
291
|
+
if (!settingsWorkDir || settingsWorkDir === ".")
|
|
292
|
+
return this.workDir;
|
|
293
|
+
const resolved = resolve(this.workDir, settingsWorkDir);
|
|
294
|
+
if (!existsSync(resolved))
|
|
295
|
+
mkdirSync(resolved, { recursive: true });
|
|
296
|
+
return resolved;
|
|
297
|
+
}
|
|
298
|
+
/** Build the shared OrchestratorContext used by all managers. */
|
|
299
|
+
buildContext() {
|
|
300
|
+
return {
|
|
301
|
+
emitter: this,
|
|
302
|
+
registry: this.registry,
|
|
303
|
+
runStore: this.runStore,
|
|
304
|
+
memoryStore: this.memoryStore,
|
|
305
|
+
logStore: this.logStore,
|
|
306
|
+
sessionStore: this.sessionStore,
|
|
307
|
+
teamStore: this.teamStore,
|
|
308
|
+
agentStore: this.agentStore,
|
|
309
|
+
hooks: this.hookRegistry,
|
|
310
|
+
config: this.config,
|
|
311
|
+
workDir: this.workDir,
|
|
312
|
+
agentWorkDir: this.getAgentWorkDir(),
|
|
313
|
+
polpoDir: this.polpoDir,
|
|
314
|
+
assessFn: this.assessFn,
|
|
315
|
+
spawner: this.spawner,
|
|
316
|
+
// Shell-specific ports (Node.js implementations)
|
|
317
|
+
killProcess: (pid, signal) => { try {
|
|
318
|
+
process.kill(pid, (signal ?? "SIGTERM"));
|
|
319
|
+
}
|
|
320
|
+
catch { /* already dead */ } },
|
|
321
|
+
loadConfig: () => loadPolpoConfig(this.polpoDir),
|
|
322
|
+
saveConfig: (config) => savePolpoConfig(this.polpoDir, config),
|
|
323
|
+
queryLLM: async (prompt, model) => {
|
|
324
|
+
const { queryText, queryTextWithFallback, resolveModelSpec, estimateCost } = await import("../llm/pi-client.js");
|
|
325
|
+
const { withRetry } = await import("../llm/retry.js");
|
|
326
|
+
// If ModelConfig with fallbacks, use fallback-aware query
|
|
327
|
+
if (model && typeof model === "object" && model.fallbacks?.length > 0) {
|
|
328
|
+
return withRetry(async () => {
|
|
329
|
+
const result = await queryTextWithFallback(prompt, model, { gateway: this.gatewayConfig });
|
|
330
|
+
let costUsd;
|
|
331
|
+
if (result.usage) {
|
|
332
|
+
try {
|
|
333
|
+
costUsd = estimateCost(result.model, result.usage).totalCost;
|
|
334
|
+
}
|
|
335
|
+
catch { }
|
|
336
|
+
}
|
|
337
|
+
return { text: result.text, usage: result.usage, model: result.model, usedSpec: result.usedSpec, costUsd };
|
|
338
|
+
}, { maxRetries: 1 });
|
|
339
|
+
}
|
|
340
|
+
const spec = resolveModelSpec(model);
|
|
341
|
+
return withRetry(async () => {
|
|
342
|
+
const result = await queryText(prompt, spec, undefined, { gateway: this.gatewayConfig });
|
|
343
|
+
let costUsd;
|
|
344
|
+
if (result.usage) {
|
|
345
|
+
try {
|
|
346
|
+
costUsd = estimateCost(result.model, result.usage).totalCost;
|
|
347
|
+
}
|
|
348
|
+
catch { }
|
|
349
|
+
}
|
|
350
|
+
return { text: result.text, usage: result.usage, model: result.model, costUsd };
|
|
351
|
+
}, { maxRetries: 2 });
|
|
352
|
+
},
|
|
353
|
+
findLogForTask: (polpoDir, taskId, runId) => findLogForTask(polpoDir, taskId, runId),
|
|
354
|
+
buildExecutionSummary: (logPath) => buildExecutionSummary(logPath),
|
|
355
|
+
validateProviderKeys: (modelSpecs) => validateProviderKeys(modelSpecs),
|
|
356
|
+
readRunLog: (runId) => {
|
|
357
|
+
const logPath = join(this.polpoDir, "logs", `run-${runId}.jsonl`);
|
|
358
|
+
if (!existsSync(logPath))
|
|
359
|
+
return null;
|
|
360
|
+
return readFileSync(logPath, "utf-8");
|
|
361
|
+
},
|
|
362
|
+
// Inject Drizzle stores when storage is "sqlite" or "postgres"
|
|
363
|
+
...(this.drizzleStores ? {
|
|
364
|
+
approvalStore: this.drizzleStores.approvalStore,
|
|
365
|
+
checkpointStore: this.drizzleStores.checkpointStore,
|
|
366
|
+
delayStore: this.drizzleStores.delayStore,
|
|
367
|
+
configStore: this.drizzleStores.configStore,
|
|
368
|
+
} : {}),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
/** Create manager instances with shared context. */
|
|
372
|
+
async initManagers() {
|
|
373
|
+
const ctx = this.buildContext();
|
|
374
|
+
this.agentMgr = new AgentManager(ctx);
|
|
375
|
+
this.taskMgr = new TaskManager(ctx);
|
|
376
|
+
this.missionExec = new MissionExecutor(ctx, this.taskMgr, this.agentMgr);
|
|
377
|
+
await this.missionExec.ready;
|
|
378
|
+
this.runner = new TaskRunner(ctx);
|
|
379
|
+
this.assessor = new AssessmentOrchestrator(ctx);
|
|
380
|
+
// Initialize approval gates if configured
|
|
381
|
+
if (this.config.settings.approvalGates && this.config.settings.approvalGates.length > 0) {
|
|
382
|
+
const approvalStore = ctx.approvalStore ?? new FileApprovalStore(this.polpoDir);
|
|
383
|
+
this.approvalMgr = new ApprovalManager(ctx, approvalStore);
|
|
384
|
+
this.approvalMgr.init();
|
|
385
|
+
}
|
|
386
|
+
// Initialize escalation manager if configured
|
|
387
|
+
if (this.config.settings.escalationPolicy) {
|
|
388
|
+
this.escalationMgr = new EscalationManager(ctx, this.approvalMgr);
|
|
389
|
+
this.escalationMgr.init();
|
|
390
|
+
}
|
|
391
|
+
// Initialize SLA monitor if configured
|
|
392
|
+
if (this.config.settings.sla) {
|
|
393
|
+
this.slaMonitor = new SLAMonitor(ctx, this.config.settings.sla);
|
|
394
|
+
this.slaMonitor.init();
|
|
395
|
+
}
|
|
396
|
+
// Initialize quality controller (always available — zero-cost when unused)
|
|
397
|
+
this.qualityController = new QualityController(ctx);
|
|
398
|
+
this.qualityController.init();
|
|
399
|
+
this.missionExec.setQualityController(this.qualityController);
|
|
400
|
+
// Initialize scheduler (always available — zero cost when no schedules exist)
|
|
401
|
+
if (this.config.settings.enableScheduler !== false) {
|
|
402
|
+
this.scheduler = new Scheduler(ctx);
|
|
403
|
+
this.scheduler.setExecutor((missionId) => this.missionExec.executeMission(missionId));
|
|
404
|
+
this.scheduler.init();
|
|
405
|
+
}
|
|
406
|
+
// Build the shared action executor (used by task watchers)
|
|
407
|
+
const actionExecutor = this.buildActionExecutor(ctx);
|
|
408
|
+
// Initialize task watcher manager (always available — zero cost when no watchers)
|
|
409
|
+
this.watcherMgr = new TaskWatcherManager(this);
|
|
410
|
+
this.watcherMgr.setActionExecutor(actionExecutor);
|
|
411
|
+
this.watcherMgr.start();
|
|
412
|
+
// Build the deadlock resolver port (wraps the shell's deadlock-resolver module)
|
|
413
|
+
const deadlockResolver = {
|
|
414
|
+
isResolving,
|
|
415
|
+
analyzeBlockedTasks,
|
|
416
|
+
resolveDeadlock: (analysis, facade) => resolveDeadlock(analysis, this),
|
|
417
|
+
};
|
|
418
|
+
// Create the pure orchestration engine
|
|
419
|
+
this.engine = new OrchestratorEngine({
|
|
420
|
+
ctx,
|
|
421
|
+
taskManager: this.taskMgr,
|
|
422
|
+
agentManager: this.agentMgr,
|
|
423
|
+
missionExecutor: this.missionExec,
|
|
424
|
+
taskRunner: this.runner,
|
|
425
|
+
assessmentOrchestrator: this.assessor,
|
|
426
|
+
approvalManager: this.approvalMgr,
|
|
427
|
+
scheduler: this.scheduler,
|
|
428
|
+
slaMonitor: this.slaMonitor,
|
|
429
|
+
qualityController: this.qualityController,
|
|
430
|
+
escalationManager: this.escalationMgr,
|
|
431
|
+
deadlockResolver,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Build the action executor callback — handles create_task, execute_mission,
|
|
436
|
+
* run_script actions triggered by task watchers.
|
|
437
|
+
*/
|
|
438
|
+
buildActionExecutor(ctx) {
|
|
439
|
+
return async (action) => {
|
|
440
|
+
switch (action.type) {
|
|
441
|
+
case "create_task": {
|
|
442
|
+
const task = await this.addTask({
|
|
443
|
+
title: action.title,
|
|
444
|
+
description: action.description,
|
|
445
|
+
assignTo: action.assignTo,
|
|
446
|
+
expectations: action.expectations,
|
|
447
|
+
});
|
|
448
|
+
return `Task created: [${task.id}] "${task.title}" → ${task.assignTo}`;
|
|
449
|
+
}
|
|
450
|
+
case "execute_mission": {
|
|
451
|
+
const mission = await resolveMissionStore({ registry: this.registry }).getMission(action.missionId);
|
|
452
|
+
if (!mission)
|
|
453
|
+
throw new Error(`Mission "${action.missionId}" not found`);
|
|
454
|
+
const result = await this.missionExec.executeMission(action.missionId);
|
|
455
|
+
return `Mission "${mission.name}" executed: ${result.tasks.length} tasks created`;
|
|
456
|
+
}
|
|
457
|
+
case "run_script": {
|
|
458
|
+
const { execSync } = await import("node:child_process");
|
|
459
|
+
const timeout = action.timeoutMs ?? 30_000;
|
|
460
|
+
const result = execSync(action.command, {
|
|
461
|
+
cwd: ctx.agentWorkDir,
|
|
462
|
+
timeout,
|
|
463
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
464
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
465
|
+
});
|
|
466
|
+
return `Script completed: ${result.toString().trim().slice(0, 200)}`;
|
|
467
|
+
}
|
|
468
|
+
default:
|
|
469
|
+
throw new Error(`Unknown action type: ${action.type}`);
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Initialize for interactive mode.
|
|
475
|
+
* Creates .polpo dir and a minimal config from provided team info.
|
|
476
|
+
*/
|
|
477
|
+
async initInteractive(project, teams) {
|
|
478
|
+
const teamsArray = Array.isArray(teams) ? teams : [teams];
|
|
479
|
+
if (!existsSync(this.polpoDir)) {
|
|
480
|
+
mkdirSync(this.polpoDir, { recursive: true });
|
|
481
|
+
}
|
|
482
|
+
// Load .polpo/.env so ${VAR} references resolve correctly
|
|
483
|
+
loadEnvFile(this.polpoDir);
|
|
484
|
+
// Load persistent config if available
|
|
485
|
+
const polpoConfig = loadPolpoConfig(this.polpoDir);
|
|
486
|
+
const settings = polpoConfig?.settings ?? { maxRetries: 2, workDir: ".", logLevel: "normal" };
|
|
487
|
+
const storageBackend = settings.storage;
|
|
488
|
+
const dbUrl = settings.databaseUrl ?? process.env.DATABASE_URL;
|
|
489
|
+
const stores = this.injectedStore
|
|
490
|
+
? { task: this.injectedStore, run: this.injectedRunStore }
|
|
491
|
+
: await this.createStores(storageBackend, dbUrl);
|
|
492
|
+
this.registry = stores.task;
|
|
493
|
+
this.runStore = stores.run;
|
|
494
|
+
// Use Drizzle-provided stores when available, otherwise fall back to file-based
|
|
495
|
+
if ("logStore" in stores && stores.logStore) {
|
|
496
|
+
this.logStore = stores.logStore;
|
|
497
|
+
await this.logStore.startSession();
|
|
498
|
+
this.setLogSink(this.logStore);
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
await this.initLogStore();
|
|
502
|
+
}
|
|
503
|
+
if ("sessionStore" in stores && stores.sessionStore) {
|
|
504
|
+
this.sessionStore = stores.sessionStore;
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
await this.initSessionStore();
|
|
508
|
+
}
|
|
509
|
+
this.memoryStore = ("memoryStore" in stores && stores.memoryStore)
|
|
510
|
+
? stores.memoryStore
|
|
511
|
+
: new FileMemoryStore(this.polpoDir);
|
|
512
|
+
// Team & Agent stores — Drizzle when available, otherwise file-based
|
|
513
|
+
this.teamStore = this.drizzleStores?.teamStore ?? new FileTeamStore(this.polpoDir);
|
|
514
|
+
this.agentStore = this.drizzleStores?.agentStore ?? new FileAgentStore(this.polpoDir);
|
|
515
|
+
// Populate stores with the teams passed to initInteractive
|
|
516
|
+
// (this is project creation, not migration — teams come from the caller)
|
|
517
|
+
await this.populateStores(teamsArray);
|
|
518
|
+
this.config = {
|
|
519
|
+
version: "1",
|
|
520
|
+
project: polpoConfig?.project ?? project,
|
|
521
|
+
teams: [], // populated by syncConfigCache() from stores
|
|
522
|
+
tasks: [],
|
|
523
|
+
settings,
|
|
524
|
+
providers: polpoConfig?.providers,
|
|
525
|
+
};
|
|
526
|
+
// Apply provider overrides and allowlist
|
|
527
|
+
if (this.config.providers) {
|
|
528
|
+
setProviderOverrides(this.config.providers);
|
|
529
|
+
}
|
|
530
|
+
if (this.config.settings.modelAllowlist) {
|
|
531
|
+
setModelAllowlist(this.config.settings.modelAllowlist);
|
|
532
|
+
}
|
|
533
|
+
// Configure LLM gateway from settings
|
|
534
|
+
const gatewaySettings = this.config.settings.gateway;
|
|
535
|
+
if (gatewaySettings) {
|
|
536
|
+
this.gatewayConfig = {
|
|
537
|
+
url: gatewaySettings.url,
|
|
538
|
+
apiKey: gatewaySettings.apiKeyEnv ? process.env[gatewaySettings.apiKeyEnv] : undefined,
|
|
539
|
+
headers: gatewaySettings.headers,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
await this.initManagers();
|
|
543
|
+
// Sync config.teams from stores (authoritative source — agents.json / teams.json)
|
|
544
|
+
await this.agentMgr.syncConfigCache();
|
|
545
|
+
this.initVaultStore();
|
|
546
|
+
this.playbookStore = this.drizzleStores?.playbookStore ?? new FilePlaybookStore(this.workDir, this.polpoDir);
|
|
547
|
+
this.interactive = true;
|
|
548
|
+
await this.registry.setState({
|
|
549
|
+
project,
|
|
550
|
+
teams: this.config.teams,
|
|
551
|
+
startedAt: new Date().toISOString(),
|
|
552
|
+
});
|
|
553
|
+
// Recover any tasks left in limbo from a previous crash
|
|
554
|
+
const recovered = await this.runner.recoverOrphanedTasks();
|
|
555
|
+
if (recovered > 0) {
|
|
556
|
+
this.emit("log", { level: "warn", message: `Recovered ${recovered} orphaned task(s) from previous session` });
|
|
557
|
+
}
|
|
558
|
+
// Watch polpo.json for changes and auto-reload
|
|
559
|
+
this.startConfigWatcher();
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Watch `.polpo/polpo.json` for changes and auto-reload the config.
|
|
563
|
+
* Uses a 500ms debounce to avoid reloading multiple times on rapid saves.
|
|
564
|
+
*/
|
|
565
|
+
startConfigWatcher() {
|
|
566
|
+
const configPath = join(this.polpoDir, "polpo.json");
|
|
567
|
+
if (!existsSync(configPath))
|
|
568
|
+
return;
|
|
569
|
+
try {
|
|
570
|
+
this.configWatcher = watch(configPath, () => {
|
|
571
|
+
// Debounce: wait 500ms after the last change event
|
|
572
|
+
if (this.configReloadTimer)
|
|
573
|
+
clearTimeout(this.configReloadTimer);
|
|
574
|
+
this.configReloadTimer = setTimeout(() => {
|
|
575
|
+
this.emit("log", { level: "info", message: "[watch] polpo.json changed on disk — auto-reloading config" });
|
|
576
|
+
this.reloadConfig().catch(() => { });
|
|
577
|
+
}, 500);
|
|
578
|
+
});
|
|
579
|
+
this.emit("log", { level: "info", message: "[watch] Watching polpo.json for changes" });
|
|
580
|
+
}
|
|
581
|
+
catch (err) {
|
|
582
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
583
|
+
this.emit("log", { level: "warn", message: `[watch] Failed to watch polpo.json: ${msg}` });
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// ── Task Management (delegates to OrchestratorEngine → TaskManager) ──
|
|
587
|
+
async addTask(opts) { return this.engine.addTask(opts); }
|
|
588
|
+
async updateTaskDescription(taskId, description) { return this.engine.updateTaskDescription(taskId, description); }
|
|
589
|
+
async updateTaskAssignment(taskId, agentName) { return this.engine.updateTaskAssignment(taskId, agentName); }
|
|
590
|
+
async updateTaskExpectations(taskId, expectations) { return this.engine.updateTaskExpectations(taskId, expectations); }
|
|
591
|
+
async retryTask(taskId) { return this.engine.retryTask(taskId); }
|
|
592
|
+
reassessTask(taskId) { return this.engine.reassessTask(taskId); }
|
|
593
|
+
async killTask(taskId) { return this.engine.killTask(taskId); }
|
|
594
|
+
async deleteTask(taskId) { return this.engine.deleteTask(taskId); }
|
|
595
|
+
async abortGroup(group) { return this.engine.abortGroup(group); }
|
|
596
|
+
async clearTasks(filter) { return this.engine.clearTasks(filter); }
|
|
597
|
+
async forceFailTask(taskId) { return this.engine.forceFailTask(taskId); }
|
|
598
|
+
// ── Approval Management (delegates to OrchestratorEngine) ──
|
|
599
|
+
async approveRequest(requestId, resolvedBy, note) {
|
|
600
|
+
return this.engine.approveRequest(requestId, resolvedBy, note);
|
|
601
|
+
}
|
|
602
|
+
async rejectRequest(requestId, feedback, resolvedBy) {
|
|
603
|
+
return this.engine.rejectRequest(requestId, feedback, resolvedBy);
|
|
604
|
+
}
|
|
605
|
+
async canRejectRequest(requestId) {
|
|
606
|
+
return this.engine.canRejectRequest(requestId);
|
|
607
|
+
}
|
|
608
|
+
async getPendingApprovals() {
|
|
609
|
+
return this.engine.getPendingApprovals();
|
|
610
|
+
}
|
|
611
|
+
async getAllApprovals(status) {
|
|
612
|
+
return this.engine.getAllApprovals(status);
|
|
613
|
+
}
|
|
614
|
+
async getApprovalRequest(id) {
|
|
615
|
+
return this.engine.getApprovalRequest(id);
|
|
616
|
+
}
|
|
617
|
+
// ── Store Accessors ──
|
|
618
|
+
getStore() { return this.registry; }
|
|
619
|
+
getRunStore() { return this.runStore; }
|
|
620
|
+
getPolpoDir() { return this.polpoDir; }
|
|
621
|
+
getMemoryStore() { return this.memoryStore; }
|
|
622
|
+
getVaultStore() { return this.vaultStore; }
|
|
623
|
+
getPlaybookStore() { return this.playbookStore; }
|
|
624
|
+
getTeamStore() { return this.teamStore; }
|
|
625
|
+
getFs() { return this.fs; }
|
|
626
|
+
getShell() { return this.shell; }
|
|
627
|
+
getGatewayConfig() { return this.gatewayConfig; }
|
|
628
|
+
getAgentStore() { return this.agentStore; }
|
|
629
|
+
/**
|
|
630
|
+
* Initialize the vault store.
|
|
631
|
+
* When storage is "sqlite" or "postgres", uses DrizzleVaultStore (AES-256-GCM encrypted in DB).
|
|
632
|
+
* Otherwise falls back to EncryptedVaultStore (file-based, .polpo/vault.enc).
|
|
633
|
+
* Key: POLPO_VAULT_KEY env var or auto-generated ~/.polpo/vault.key.
|
|
634
|
+
*/
|
|
635
|
+
initVaultStore() {
|
|
636
|
+
try {
|
|
637
|
+
this.vaultStore = this.drizzleStores?.vaultStore ?? new EncryptedVaultStore(this.polpoDir);
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
641
|
+
this.emit("log", { level: "warn", message: `Vault store init failed: ${msg}. Vault features disabled.` });
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
// ── Agent Management (delegates to OrchestratorEngine → AgentManager) ──
|
|
645
|
+
async getAgents() { return this.engine.getAgents(); }
|
|
646
|
+
async getTeams() { return this.engine.getTeams(); }
|
|
647
|
+
async getTeam(name) { return this.engine.getTeam(name); }
|
|
648
|
+
getConfig() { return this.config; }
|
|
649
|
+
get isInitialized() { return this.interactive; }
|
|
650
|
+
async addTeam(team) { return this.engine.addTeam(team); }
|
|
651
|
+
async removeTeam(name) { return this.engine.removeTeam(name); }
|
|
652
|
+
async renameTeam(oldName, newName) { return this.engine.renameTeam(oldName, newName); }
|
|
653
|
+
async addAgent(agent, teamName) { return this.engine.addAgent(agent, teamName); }
|
|
654
|
+
async removeAgent(name) { return this.engine.removeAgent(name); }
|
|
655
|
+
async updateAgent(name, updates) { return this.engine.updateAgent(name, updates); }
|
|
656
|
+
async findAgentTeam(name) { return this.engine.findAgentTeam(name); }
|
|
657
|
+
async addVolatileAgent(agent, group) { return this.engine.addVolatileAgent(agent, group); }
|
|
658
|
+
async cleanupVolatileAgents(group) { return this.engine.cleanupVolatileAgents(group); }
|
|
659
|
+
// ─── Mission Management (delegates to OrchestratorEngine → MissionExecutor) ──
|
|
660
|
+
async saveMission(opts) { return this.engine.saveMission(opts); }
|
|
661
|
+
async getMission(missionId) { return this.engine.getMission(missionId); }
|
|
662
|
+
async getMissionByName(name) { return this.engine.getMissionByName(name); }
|
|
663
|
+
async getAllMissions() { return this.engine.getAllMissions(); }
|
|
664
|
+
async updateMission(missionId, updates) { return this.engine.updateMission(missionId, updates); }
|
|
665
|
+
async deleteMission(missionId) { return this.engine.deleteMission(missionId); }
|
|
666
|
+
// ─── Atomic Mission Data Operations (delegates to OrchestratorEngine → MissionExecutor) ──
|
|
667
|
+
async addMissionTask(missionId, task) {
|
|
668
|
+
return this.engine.addMissionTask(missionId, task);
|
|
669
|
+
}
|
|
670
|
+
async updateMissionTask(missionId, taskTitle, updates) {
|
|
671
|
+
return this.engine.updateMissionTask(missionId, taskTitle, updates);
|
|
672
|
+
}
|
|
673
|
+
async removeMissionTask(missionId, taskTitle) {
|
|
674
|
+
return this.engine.removeMissionTask(missionId, taskTitle);
|
|
675
|
+
}
|
|
676
|
+
async reorderMissionTasks(missionId, titles) {
|
|
677
|
+
return this.engine.reorderMissionTasks(missionId, titles);
|
|
678
|
+
}
|
|
679
|
+
async addMissionCheckpoint(missionId, cp) {
|
|
680
|
+
return this.engine.addMissionCheckpoint(missionId, cp);
|
|
681
|
+
}
|
|
682
|
+
async updateMissionCheckpoint(missionId, name, updates) {
|
|
683
|
+
return this.engine.updateMissionCheckpoint(missionId, name, updates);
|
|
684
|
+
}
|
|
685
|
+
async removeMissionCheckpoint(missionId, name) {
|
|
686
|
+
return this.engine.removeMissionCheckpoint(missionId, name);
|
|
687
|
+
}
|
|
688
|
+
async addMissionQualityGate(missionId, gate) {
|
|
689
|
+
return this.engine.addMissionQualityGate(missionId, gate);
|
|
690
|
+
}
|
|
691
|
+
async updateMissionQualityGate(missionId, name, updates) {
|
|
692
|
+
return this.engine.updateMissionQualityGate(missionId, name, updates);
|
|
693
|
+
}
|
|
694
|
+
async removeMissionQualityGate(missionId, name) {
|
|
695
|
+
return this.engine.removeMissionQualityGate(missionId, name);
|
|
696
|
+
}
|
|
697
|
+
async addMissionDelay(missionId, delay) {
|
|
698
|
+
return this.engine.addMissionDelay(missionId, delay);
|
|
699
|
+
}
|
|
700
|
+
async updateMissionDelay(missionId, name, updates) {
|
|
701
|
+
return this.engine.updateMissionDelay(missionId, name, updates);
|
|
702
|
+
}
|
|
703
|
+
async removeMissionDelay(missionId, name) {
|
|
704
|
+
return this.engine.removeMissionDelay(missionId, name);
|
|
705
|
+
}
|
|
706
|
+
async addMissionTeamMember(missionId, member) {
|
|
707
|
+
return this.engine.addMissionTeamMember(missionId, member);
|
|
708
|
+
}
|
|
709
|
+
async updateMissionTeamMember(missionId, memberName, updates) {
|
|
710
|
+
return this.engine.updateMissionTeamMember(missionId, memberName, updates);
|
|
711
|
+
}
|
|
712
|
+
async removeMissionTeamMember(missionId, memberName) {
|
|
713
|
+
return this.engine.removeMissionTeamMember(missionId, memberName);
|
|
714
|
+
}
|
|
715
|
+
async updateMissionNotifications(missionId, notifications) {
|
|
716
|
+
return this.engine.updateMissionNotifications(missionId, notifications);
|
|
717
|
+
}
|
|
718
|
+
// ─── Shared Memory (delegates to OrchestratorEngine) ───
|
|
719
|
+
/** Check if shared memory exists. */
|
|
720
|
+
async hasMemory() { return this.engine.hasMemory(); }
|
|
721
|
+
/** Get the full shared memory content. */
|
|
722
|
+
async getMemory() { return this.engine.getMemory(); }
|
|
723
|
+
/** Overwrite the shared memory. */
|
|
724
|
+
async saveMemory(content) { return this.engine.saveMemory(content); }
|
|
725
|
+
/** Append a line to the shared memory. */
|
|
726
|
+
async appendMemory(line) { return this.engine.appendMemory(line); }
|
|
727
|
+
/** Replace a unique substring in the shared memory. */
|
|
728
|
+
async updateMemory(oldText, newText) { return this.engine.updateMemory(oldText, newText); }
|
|
729
|
+
// ─── Agent Memory (delegates to OrchestratorEngine) ───
|
|
730
|
+
/** Check if memory exists for a specific agent. */
|
|
731
|
+
async hasAgentMemory(agentName) { return this.engine.hasAgentMemory(agentName); }
|
|
732
|
+
/** Get the memory content for a specific agent. */
|
|
733
|
+
async getAgentMemory(agentName) { return this.engine.getAgentMemory(agentName); }
|
|
734
|
+
/** Overwrite the memory for a specific agent. */
|
|
735
|
+
async saveAgentMemory(agentName, content) { return this.engine.saveAgentMemory(agentName, content); }
|
|
736
|
+
/** Append a line to a specific agent's memory. */
|
|
737
|
+
async appendAgentMemory(agentName, line) { return this.engine.appendAgentMemory(agentName, line); }
|
|
738
|
+
/** Replace a unique substring in a specific agent's memory. */
|
|
739
|
+
async updateAgentMemory(agentName, oldText, newText) { return this.engine.updateAgentMemory(agentName, oldText, newText); }
|
|
740
|
+
/** Get the persistent log store. */
|
|
741
|
+
getLogStore() {
|
|
742
|
+
return this.logStore;
|
|
743
|
+
}
|
|
744
|
+
/** Initialize the persistent log store and wire it as event sink. */
|
|
745
|
+
async initLogStore() {
|
|
746
|
+
this.logStore = new FileLogStore(this.polpoDir);
|
|
747
|
+
await this.logStore.startSession();
|
|
748
|
+
this.setLogSink(this.logStore);
|
|
749
|
+
// Auto-prune: keep last 20 sessions
|
|
750
|
+
try {
|
|
751
|
+
await this.logStore.prune(20);
|
|
752
|
+
}
|
|
753
|
+
catch { /* best-effort: non-critical */ }
|
|
754
|
+
}
|
|
755
|
+
/** Get the chat session store. */
|
|
756
|
+
getSessionStore() {
|
|
757
|
+
return this.sessionStore;
|
|
758
|
+
}
|
|
759
|
+
/** Initialize the chat session store. */
|
|
760
|
+
async initSessionStore() {
|
|
761
|
+
this.sessionStore = new FileSessionStore(this.polpoDir);
|
|
762
|
+
try {
|
|
763
|
+
await this.sessionStore.prune(20);
|
|
764
|
+
}
|
|
765
|
+
catch { /* best-effort: non-critical */ }
|
|
766
|
+
}
|
|
767
|
+
// ─── Mission Resume / Execute (delegates to OrchestratorEngine → MissionExecutor) ──
|
|
768
|
+
async getResumableMissions() { return this.engine.getResumableMissions(); }
|
|
769
|
+
async resumeMission(missionId, opts) { return this.engine.resumeMission(missionId, opts); }
|
|
770
|
+
async executeMission(missionId) { return this.engine.executeMission(missionId); }
|
|
771
|
+
// ─── Checkpoints (delegates to OrchestratorEngine) ──
|
|
772
|
+
/** Get all active (unresumed) checkpoints across all mission groups. */
|
|
773
|
+
getActiveCheckpoints() { return this.engine.getActiveCheckpoints(); }
|
|
774
|
+
/** Resume a checkpoint by mission group name and checkpoint name. Returns true if resumed. */
|
|
775
|
+
async resumeCheckpoint(group, checkpointName) {
|
|
776
|
+
return this.engine.resumeCheckpoint(group, checkpointName);
|
|
777
|
+
}
|
|
778
|
+
/** Resume a checkpoint by mission ID and checkpoint name. Returns true if resumed. */
|
|
779
|
+
async resumeCheckpointByMissionId(missionId, checkpointName) {
|
|
780
|
+
return this.engine.resumeCheckpointByMissionId(missionId, checkpointName);
|
|
781
|
+
}
|
|
782
|
+
// ─── Delays (delegates to OrchestratorEngine) ─────
|
|
783
|
+
/** Get all active (unexpired) delays across all mission groups. */
|
|
784
|
+
getActiveDelays() { return this.engine.getActiveDelays(); }
|
|
785
|
+
/** Stop the supervisor loop (non-graceful — use gracefulStop for clean shutdown) */
|
|
786
|
+
stop() {
|
|
787
|
+
this.stopped = true;
|
|
788
|
+
this.engine?.stop();
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Graceful shutdown: SIGTERM all runner subprocesses, wait for them to write results,
|
|
792
|
+
* preserve completed work, leave in-progress tasks for recovery on restart.
|
|
793
|
+
*/
|
|
794
|
+
async gracefulStop(timeoutMs = 5000) {
|
|
795
|
+
await this.hookRegistry.runBefore("orchestrator:shutdown", {});
|
|
796
|
+
this.stopped = true;
|
|
797
|
+
const activeRuns = await this.runStore.getActiveRuns();
|
|
798
|
+
if (activeRuns.length > 0) {
|
|
799
|
+
this.emit("log", { level: "warn", message: `Shutting down ${activeRuns.length} running agent(s)...` });
|
|
800
|
+
// Send SIGTERM to all runner subprocesses
|
|
801
|
+
for (const run of activeRuns) {
|
|
802
|
+
if (run.pid > 0) {
|
|
803
|
+
try {
|
|
804
|
+
process.kill(run.pid, "SIGTERM");
|
|
805
|
+
}
|
|
806
|
+
catch { /* already dead */ }
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
// Wait for runners to write their results
|
|
810
|
+
const deadline = Date.now() + timeoutMs;
|
|
811
|
+
while (Date.now() < deadline) {
|
|
812
|
+
const stillActive = await this.runStore.getActiveRuns();
|
|
813
|
+
if (stillActive.length === 0)
|
|
814
|
+
break;
|
|
815
|
+
await sleep(200);
|
|
816
|
+
}
|
|
817
|
+
// Force-mark any remaining active runs as killed
|
|
818
|
+
for (const run of await this.runStore.getActiveRuns()) {
|
|
819
|
+
await this.runStore.completeRun(run.id, "killed", {
|
|
820
|
+
exitCode: 1, stdout: "", stderr: "Killed during shutdown", duration: 0,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
// Only save completed work — leave killed/failed tasks in current state for recovery
|
|
825
|
+
for (const run of await this.runStore.getTerminalRuns()) {
|
|
826
|
+
const task = await this.registry.getTask(run.taskId);
|
|
827
|
+
if (run.status === "completed" && run.result?.exitCode === 0 && task && task.status !== "done") {
|
|
828
|
+
// Agent finished successfully — save result and mark done (skip async assessment)
|
|
829
|
+
try {
|
|
830
|
+
await this.registry.updateTask(run.taskId, { result: run.result });
|
|
831
|
+
if (task.status === "pending")
|
|
832
|
+
await this.registry.transition(run.taskId, "assigned");
|
|
833
|
+
if (task.status === "assigned")
|
|
834
|
+
await this.registry.transition(run.taskId, "in_progress");
|
|
835
|
+
if (task.status === "in_progress")
|
|
836
|
+
await this.registry.transition(run.taskId, "review");
|
|
837
|
+
await this.registry.transition(run.taskId, "done");
|
|
838
|
+
}
|
|
839
|
+
catch { /* leave for recovery on restart */ }
|
|
840
|
+
}
|
|
841
|
+
// For killed/failed runs: task stays in current state (in_progress, assigned, etc.)
|
|
842
|
+
// recoverOrphanedTasks() on restart will handle retry without burning retry count
|
|
843
|
+
await this.runStore.deleteRun(run.id);
|
|
844
|
+
}
|
|
845
|
+
// Clear process list in state and close stores
|
|
846
|
+
await this.registry.setState({ processes: [], completedAt: new Date().toISOString() });
|
|
847
|
+
if (this.configReloadTimer)
|
|
848
|
+
clearTimeout(this.configReloadTimer);
|
|
849
|
+
this.configWatcher?.close();
|
|
850
|
+
this.approvalMgr?.dispose();
|
|
851
|
+
this.escalationMgr?.dispose();
|
|
852
|
+
this.slaMonitor?.dispose();
|
|
853
|
+
this.qualityController?.dispose();
|
|
854
|
+
this.scheduler?.dispose();
|
|
855
|
+
await this.registry.close?.();
|
|
856
|
+
await this.runStore.close();
|
|
857
|
+
this.emit("orchestrator:shutdown", {});
|
|
858
|
+
await this.hookRegistry.runAfter("orchestrator:shutdown", {});
|
|
859
|
+
await this.logStore?.close();
|
|
860
|
+
await this.sessionStore?.close();
|
|
861
|
+
}
|
|
862
|
+
// ── Config Hot Reload ──
|
|
863
|
+
/**
|
|
864
|
+
* Reload polpo.json at runtime without restarting the server.
|
|
865
|
+
* Disposes optional subsystems (approvals, escalation, SLA,
|
|
866
|
+
* quality, scheduler) and re-initializes them from the
|
|
867
|
+
* freshly-read config. Core managers (agents, tasks, missions, runner,
|
|
868
|
+
* assessor) and stores are left untouched — live state is preserved.
|
|
869
|
+
*
|
|
870
|
+
* Returns `true` if the config was successfully reloaded.
|
|
871
|
+
*/
|
|
872
|
+
async reloadConfig() {
|
|
873
|
+
const polpoConfig = loadPolpoConfig(this.polpoDir);
|
|
874
|
+
if (!polpoConfig) {
|
|
875
|
+
this.emit("log", { level: "warn", message: "[reload] polpo.json not found or unparseable — skipping reload" });
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
this.emit("log", { level: "info", message: "[reload] Reloading configuration..." });
|
|
879
|
+
// 1. Dispose optional subsystems (scheduler is handled separately to preserve state)
|
|
880
|
+
this.qualityController?.dispose();
|
|
881
|
+
this.qualityController = undefined;
|
|
882
|
+
this.slaMonitor?.dispose();
|
|
883
|
+
this.slaMonitor = undefined;
|
|
884
|
+
this.escalationMgr?.dispose();
|
|
885
|
+
this.escalationMgr = undefined;
|
|
886
|
+
this.approvalMgr?.dispose();
|
|
887
|
+
this.approvalMgr = undefined;
|
|
888
|
+
// 2. Update config in-place (preserves the shared reference in OrchestratorContext)
|
|
889
|
+
// Settings and providers come from polpo.json; teams come from stores.
|
|
890
|
+
const newSettings = polpoConfig.settings ?? this.config.settings;
|
|
891
|
+
this.config.settings = newSettings;
|
|
892
|
+
if (polpoConfig.providers) {
|
|
893
|
+
this.config.providers = polpoConfig.providers;
|
|
894
|
+
setProviderOverrides(polpoConfig.providers);
|
|
895
|
+
}
|
|
896
|
+
if (newSettings.modelAllowlist) {
|
|
897
|
+
setModelAllowlist(newSettings.modelAllowlist);
|
|
898
|
+
}
|
|
899
|
+
// Re-configure LLM gateway from updated settings
|
|
900
|
+
if (newSettings.gateway) {
|
|
901
|
+
this.gatewayConfig = {
|
|
902
|
+
url: newSettings.gateway.url,
|
|
903
|
+
apiKey: newSettings.gateway.apiKeyEnv ? process.env[newSettings.gateway.apiKeyEnv] : undefined,
|
|
904
|
+
headers: newSettings.gateway.headers,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
// Re-sync config.teams from TeamStore/AgentStore (authoritative source)
|
|
908
|
+
await this.agentMgr.syncConfigCache();
|
|
909
|
+
// 3. Invalidate cached agent work dir and rebuild OrchestratorContext
|
|
910
|
+
this.cachedAgentWorkDir = null;
|
|
911
|
+
const ctx = this.buildContext();
|
|
912
|
+
// 4. Re-initialize optional subsystems from new config
|
|
913
|
+
// Approval gates
|
|
914
|
+
if (this.config.settings.approvalGates && this.config.settings.approvalGates.length > 0) {
|
|
915
|
+
const approvalStore = this.drizzleStores?.approvalStore ?? new FileApprovalStore(this.polpoDir);
|
|
916
|
+
this.approvalMgr = new ApprovalManager(ctx, approvalStore);
|
|
917
|
+
this.approvalMgr.init();
|
|
918
|
+
}
|
|
919
|
+
// Escalation manager
|
|
920
|
+
if (this.config.settings.escalationPolicy) {
|
|
921
|
+
this.escalationMgr = new EscalationManager(ctx, this.approvalMgr);
|
|
922
|
+
this.escalationMgr.init();
|
|
923
|
+
}
|
|
924
|
+
// SLA monitor
|
|
925
|
+
if (this.config.settings.sla) {
|
|
926
|
+
this.slaMonitor = new SLAMonitor(ctx, this.config.settings.sla);
|
|
927
|
+
this.slaMonitor.init();
|
|
928
|
+
}
|
|
929
|
+
// Quality controller (always available)
|
|
930
|
+
this.qualityController = new QualityController(ctx);
|
|
931
|
+
this.qualityController.init();
|
|
932
|
+
this.missionExec.setQualityController(this.qualityController);
|
|
933
|
+
// Scheduler — re-init without losing existing schedule state.
|
|
934
|
+
// If scheduler was already running, just refresh its mission registrations.
|
|
935
|
+
// If not, create a new one.
|
|
936
|
+
if (this.config.settings.enableScheduler !== false) {
|
|
937
|
+
if (!this.scheduler) {
|
|
938
|
+
this.scheduler = new Scheduler(ctx);
|
|
939
|
+
this.scheduler.setExecutor((missionId) => this.missionExec.executeMission(missionId));
|
|
940
|
+
}
|
|
941
|
+
this.scheduler.init();
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
this.scheduler?.dispose();
|
|
945
|
+
this.scheduler = undefined;
|
|
946
|
+
}
|
|
947
|
+
// Sync engine with updated optional subsystems
|
|
948
|
+
this.engine.setApprovalManager(this.approvalMgr);
|
|
949
|
+
this.engine.setScheduler(this.scheduler);
|
|
950
|
+
this.engine.setSLAMonitor(this.slaMonitor);
|
|
951
|
+
this.engine.setQualityController(this.qualityController);
|
|
952
|
+
this.engine.setEscalationManager(this.escalationMgr);
|
|
953
|
+
this.emit("log", { level: "info", message: "[reload] Configuration reloaded successfully" });
|
|
954
|
+
this.emit("config:reloaded", { timestamp: new Date().toISOString() });
|
|
955
|
+
return true;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Recover orphaned tasks on startup.
|
|
959
|
+
* Checks RunStore for active runs — if the runner PID is still alive,
|
|
960
|
+
* let it keep running (zero work lost). If PID is dead, clean up the run.
|
|
961
|
+
* Then requeue orphaned tasks to "pending" WITHOUT burning retry count
|
|
962
|
+
* (shutdown interrupts are not real failures).
|
|
963
|
+
*/
|
|
964
|
+
async recoverOrphanedTasks() { return this.engine.recoverOrphanedTasks(); }
|
|
965
|
+
async seedTasks() {
|
|
966
|
+
await this.taskMgr.seedTasks();
|
|
967
|
+
// Sync config cache from stores so state reflects authoritative data
|
|
968
|
+
await this.agentMgr.syncConfigCache();
|
|
969
|
+
// Also set initial state for non-interactive mode
|
|
970
|
+
await this.registry.setState({
|
|
971
|
+
project: this.config.project,
|
|
972
|
+
teams: this.config.teams,
|
|
973
|
+
startedAt: new Date().toISOString(),
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Main supervisor loop. Runs until all tasks are done/failed.
|
|
978
|
+
* In interactive mode, keeps running and waits for new tasks.
|
|
979
|
+
*/
|
|
980
|
+
async run() {
|
|
981
|
+
if (!this.interactive) {
|
|
982
|
+
await this.init();
|
|
983
|
+
await this.seedTasks();
|
|
984
|
+
}
|
|
985
|
+
this.stopped = false;
|
|
986
|
+
// Node.js-specific: catch unhandled promise rejections
|
|
987
|
+
const rejectionHandler = (reason) => {
|
|
988
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
989
|
+
this.emit("log", { level: "error", message: `Unhandled rejection in supervisor: ${msg}` });
|
|
990
|
+
};
|
|
991
|
+
await this.engine.run(this.interactive, () => { process.on("unhandledRejection", rejectionHandler); }, () => { process.removeListener("unhandledRejection", rejectionHandler); });
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Single tick of the supervisor loop. Returns true when all work is done.
|
|
995
|
+
*/
|
|
996
|
+
async tick() {
|
|
997
|
+
return this.engine.tick();
|
|
998
|
+
}
|
|
999
|
+
// Assessment pipeline delegated to AssessmentOrchestrator
|
|
1000
|
+
/** @internal — test access only */
|
|
1001
|
+
async retryOrFail(taskId, task, result) {
|
|
1002
|
+
await this.assessor.retryOrFail(taskId, task, result);
|
|
1003
|
+
}
|
|
1004
|
+
async status() {
|
|
1005
|
+
await this.init();
|
|
1006
|
+
// Emit log for CLI to consume
|
|
1007
|
+
const tasks = await this.registry.getAllTasks();
|
|
1008
|
+
const done = tasks.filter(t => t.status === "done");
|
|
1009
|
+
const failed = tasks.filter(t => t.status === "failed");
|
|
1010
|
+
this.emit("log", { level: "info", message: `Total: ${tasks.length} | Done: ${done.length} | Failed: ${failed.length}` });
|
|
1011
|
+
}
|
|
1012
|
+
/** Access the pure orchestration engine (for advanced use / testing). */
|
|
1013
|
+
getEngine() { return this.engine; }
|
|
1014
|
+
}
|
|
1015
|
+
//# sourceMappingURL=orchestrator.js.map
|