pm-orchestrator-runner 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/cli/cli-interface.d.ts +150 -0
- package/dist/cli/cli-interface.d.ts.map +1 -0
- package/dist/cli/cli-interface.js +606 -0
- package/dist/cli/cli-interface.js.map +1 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +243 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/llm-sentinel.d.ts +15 -0
- package/dist/cli/llm-sentinel.d.ts.map +1 -0
- package/dist/cli/llm-sentinel.js +184 -0
- package/dist/cli/llm-sentinel.js.map +1 -0
- package/dist/config/configuration-manager.d.ts +149 -0
- package/dist/config/configuration-manager.d.ts.map +1 -0
- package/dist/config/configuration-manager.js +241 -0
- package/dist/config/configuration-manager.js.map +1 -0
- package/dist/continuation/continuation-control-manager.d.ts +154 -0
- package/dist/continuation/continuation-control-manager.d.ts.map +1 -0
- package/dist/continuation/continuation-control-manager.js +303 -0
- package/dist/continuation/continuation-control-manager.js.map +1 -0
- package/dist/core/runner-core.d.ts +474 -0
- package/dist/core/runner-core.d.ts.map +1 -0
- package/dist/core/runner-core.js +1311 -0
- package/dist/core/runner-core.js.map +1 -0
- package/dist/errors/error-codes.d.ts +105 -0
- package/dist/errors/error-codes.d.ts.map +1 -0
- package/dist/errors/error-codes.js +198 -0
- package/dist/errors/error-codes.js.map +1 -0
- package/dist/errors/runner-error.d.ts +14 -0
- package/dist/errors/runner-error.d.ts.map +1 -0
- package/dist/errors/runner-error.js +33 -0
- package/dist/errors/runner-error.js.map +1 -0
- package/dist/evidence/evidence-manager.d.ts +112 -0
- package/dist/evidence/evidence-manager.d.ts.map +1 -0
- package/dist/evidence/evidence-manager.js +337 -0
- package/dist/evidence/evidence-manager.js.map +1 -0
- package/dist/executor/claude-code-executor.d.ts +136 -0
- package/dist/executor/claude-code-executor.d.ts.map +1 -0
- package/dist/executor/claude-code-executor.js +643 -0
- package/dist/executor/claude-code-executor.js.map +1 -0
- package/dist/executor/deterministic-executor.d.ts +40 -0
- package/dist/executor/deterministic-executor.d.ts.map +1 -0
- package/dist/executor/deterministic-executor.js +269 -0
- package/dist/executor/deterministic-executor.js.map +1 -0
- package/dist/lifecycle/lifecycle-controller.d.ts +270 -0
- package/dist/lifecycle/lifecycle-controller.d.ts.map +1 -0
- package/dist/lifecycle/lifecycle-controller.js +596 -0
- package/dist/lifecycle/lifecycle-controller.js.map +1 -0
- package/dist/limits/resource-limit-manager.d.ts +200 -0
- package/dist/limits/resource-limit-manager.d.ts.map +1 -0
- package/dist/limits/resource-limit-manager.js +376 -0
- package/dist/limits/resource-limit-manager.js.map +1 -0
- package/dist/locks/lock-manager.d.ts +116 -0
- package/dist/locks/lock-manager.d.ts.map +1 -0
- package/dist/locks/lock-manager.js +306 -0
- package/dist/locks/lock-manager.js.map +1 -0
- package/dist/logging/index.d.ts +8 -0
- package/dist/logging/index.d.ts.map +1 -0
- package/dist/logging/index.js +22 -0
- package/dist/logging/index.js.map +1 -0
- package/dist/logging/sensitive-data-masker.d.ts +90 -0
- package/dist/logging/sensitive-data-masker.d.ts.map +1 -0
- package/dist/logging/sensitive-data-masker.js +228 -0
- package/dist/logging/sensitive-data-masker.js.map +1 -0
- package/dist/logging/task-log-manager.d.ts +215 -0
- package/dist/logging/task-log-manager.d.ts.map +1 -0
- package/dist/logging/task-log-manager.js +743 -0
- package/dist/logging/task-log-manager.js.map +1 -0
- package/dist/mediation/fail-closed-runner.d.ts +131 -0
- package/dist/mediation/fail-closed-runner.d.ts.map +1 -0
- package/dist/mediation/fail-closed-runner.js +245 -0
- package/dist/mediation/fail-closed-runner.js.map +1 -0
- package/dist/mediation/llm-client-with-evidence.d.ts +123 -0
- package/dist/mediation/llm-client-with-evidence.d.ts.map +1 -0
- package/dist/mediation/llm-client-with-evidence.js +245 -0
- package/dist/mediation/llm-client-with-evidence.js.map +1 -0
- package/dist/mediation/llm-client.d.ts +102 -0
- package/dist/mediation/llm-client.d.ts.map +1 -0
- package/dist/mediation/llm-client.js +206 -0
- package/dist/mediation/llm-client.js.map +1 -0
- package/dist/mediation/llm-evidence-manager.d.ts +108 -0
- package/dist/mediation/llm-evidence-manager.d.ts.map +1 -0
- package/dist/mediation/llm-evidence-manager.js +230 -0
- package/dist/mediation/llm-evidence-manager.js.map +1 -0
- package/dist/mediation/llm-mediation-layer.d.ts +175 -0
- package/dist/mediation/llm-mediation-layer.d.ts.map +1 -0
- package/dist/mediation/llm-mediation-layer.js +315 -0
- package/dist/mediation/llm-mediation-layer.js.map +1 -0
- package/dist/mediation/llm-sentinel.d.ts +107 -0
- package/dist/mediation/llm-sentinel.d.ts.map +1 -0
- package/dist/mediation/llm-sentinel.js +187 -0
- package/dist/mediation/llm-sentinel.js.map +1 -0
- package/dist/mediation/real-llm-mediation-layer.d.ts +104 -0
- package/dist/mediation/real-llm-mediation-layer.d.ts.map +1 -0
- package/dist/mediation/real-llm-mediation-layer.js +322 -0
- package/dist/mediation/real-llm-mediation-layer.js.map +1 -0
- package/dist/mediation/verdict-reporter.d.ts +61 -0
- package/dist/mediation/verdict-reporter.d.ts.map +1 -0
- package/dist/mediation/verdict-reporter.js +178 -0
- package/dist/mediation/verdict-reporter.js.map +1 -0
- package/dist/models/enums.d.ts +133 -0
- package/dist/models/enums.d.ts.map +1 -0
- package/dist/models/enums.js +201 -0
- package/dist/models/enums.js.map +1 -0
- package/dist/models/evidence.d.ts +60 -0
- package/dist/models/evidence.d.ts.map +1 -0
- package/dist/models/evidence.js +135 -0
- package/dist/models/evidence.js.map +1 -0
- package/dist/models/execution-result.d.ts +89 -0
- package/dist/models/execution-result.d.ts.map +1 -0
- package/dist/models/execution-result.js +197 -0
- package/dist/models/execution-result.js.map +1 -0
- package/dist/models/file-lock.d.ts +62 -0
- package/dist/models/file-lock.d.ts.map +1 -0
- package/dist/models/file-lock.js +133 -0
- package/dist/models/file-lock.js.map +1 -0
- package/dist/models/index.d.ts +12 -0
- package/dist/models/index.d.ts.map +1 -0
- package/dist/models/index.js +91 -0
- package/dist/models/index.js.map +1 -0
- package/dist/models/repl/index.d.ts +7 -0
- package/dist/models/repl/index.d.ts.map +1 -0
- package/dist/models/repl/index.js +32 -0
- package/dist/models/repl/index.js.map +1 -0
- package/dist/models/repl/model-registry.d.ts +73 -0
- package/dist/models/repl/model-registry.d.ts.map +1 -0
- package/dist/models/repl/model-registry.js +116 -0
- package/dist/models/repl/model-registry.js.map +1 -0
- package/dist/models/repl/repl-state.d.ts +86 -0
- package/dist/models/repl/repl-state.d.ts.map +1 -0
- package/dist/models/repl/repl-state.js +152 -0
- package/dist/models/repl/repl-state.js.map +1 -0
- package/dist/models/repl/task-log.d.ts +247 -0
- package/dist/models/repl/task-log.d.ts.map +1 -0
- package/dist/models/repl/task-log.js +178 -0
- package/dist/models/repl/task-log.js.map +1 -0
- package/dist/models/session.d.ts +71 -0
- package/dist/models/session.d.ts.map +1 -0
- package/dist/models/session.js +140 -0
- package/dist/models/session.js.map +1 -0
- package/dist/models/supporting.d.ts +97 -0
- package/dist/models/supporting.d.ts.map +1 -0
- package/dist/models/supporting.js +208 -0
- package/dist/models/supporting.js.map +1 -0
- package/dist/models/task.d.ts +77 -0
- package/dist/models/task.d.ts.map +1 -0
- package/dist/models/task.js +170 -0
- package/dist/models/task.js.map +1 -0
- package/dist/output/output-control-manager.d.ts +217 -0
- package/dist/output/output-control-manager.d.ts.map +1 -0
- package/dist/output/output-control-manager.js +378 -0
- package/dist/output/output-control-manager.js.map +1 -0
- package/dist/pool/agent-pool.d.ts +284 -0
- package/dist/pool/agent-pool.d.ts.map +1 -0
- package/dist/pool/agent-pool.js +451 -0
- package/dist/pool/agent-pool.js.map +1 -0
- package/dist/repl/commands/index.d.ts +12 -0
- package/dist/repl/commands/index.d.ts.map +1 -0
- package/dist/repl/commands/index.js +26 -0
- package/dist/repl/commands/index.js.map +1 -0
- package/dist/repl/commands/init.d.ts +31 -0
- package/dist/repl/commands/init.d.ts.map +1 -0
- package/dist/repl/commands/init.js +234 -0
- package/dist/repl/commands/init.js.map +1 -0
- package/dist/repl/commands/keys.d.ts +63 -0
- package/dist/repl/commands/keys.d.ts.map +1 -0
- package/dist/repl/commands/keys.js +114 -0
- package/dist/repl/commands/keys.js.map +1 -0
- package/dist/repl/commands/logs.d.ts +91 -0
- package/dist/repl/commands/logs.d.ts.map +1 -0
- package/dist/repl/commands/logs.js +200 -0
- package/dist/repl/commands/logs.js.map +1 -0
- package/dist/repl/commands/model.d.ts +85 -0
- package/dist/repl/commands/model.d.ts.map +1 -0
- package/dist/repl/commands/model.js +225 -0
- package/dist/repl/commands/model.js.map +1 -0
- package/dist/repl/commands/models.d.ts +50 -0
- package/dist/repl/commands/models.d.ts.map +1 -0
- package/dist/repl/commands/models.js +180 -0
- package/dist/repl/commands/models.js.map +1 -0
- package/dist/repl/commands/provider.d.ts +79 -0
- package/dist/repl/commands/provider.d.ts.map +1 -0
- package/dist/repl/commands/provider.js +291 -0
- package/dist/repl/commands/provider.js.map +1 -0
- package/dist/repl/commands/session.d.ts +50 -0
- package/dist/repl/commands/session.d.ts.map +1 -0
- package/dist/repl/commands/session.js +152 -0
- package/dist/repl/commands/session.js.map +1 -0
- package/dist/repl/commands/status.d.ts +55 -0
- package/dist/repl/commands/status.d.ts.map +1 -0
- package/dist/repl/commands/status.js +182 -0
- package/dist/repl/commands/status.js.map +1 -0
- package/dist/repl/index.d.ts +6 -0
- package/dist/repl/index.d.ts.map +1 -0
- package/dist/repl/index.js +25 -0
- package/dist/repl/index.js.map +1 -0
- package/dist/repl/repl-interface.d.ts +371 -0
- package/dist/repl/repl-interface.d.ts.map +1 -0
- package/dist/repl/repl-interface.js +1214 -0
- package/dist/repl/repl-interface.js.map +1 -0
- package/dist/session/session-manager.d.ts +85 -0
- package/dist/session/session-manager.d.ts.map +1 -0
- package/dist/session/session-manager.js +217 -0
- package/dist/session/session-manager.js.map +1 -0
- package/dist/supervisor/executor-supervisor.d.ts +90 -0
- package/dist/supervisor/executor-supervisor.d.ts.map +1 -0
- package/dist/supervisor/executor-supervisor.js +223 -0
- package/dist/supervisor/executor-supervisor.js.map +1 -0
- package/dist/supervisor/index.d.ts +5 -0
- package/dist/supervisor/index.d.ts.map +1 -0
- package/dist/supervisor/index.js +9 -0
- package/dist/supervisor/index.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,1311 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Runner Core
|
|
4
|
+
* Based on 04_COMPONENTS.md L196-240
|
|
5
|
+
*
|
|
6
|
+
* Responsible for:
|
|
7
|
+
* - Orchestrating all components
|
|
8
|
+
* - Managing lifecycle execution
|
|
9
|
+
* - Task coordination
|
|
10
|
+
* - Resource management
|
|
11
|
+
* - Error handling
|
|
12
|
+
* - Evidence collection
|
|
13
|
+
* - Session state management
|
|
14
|
+
*/
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.RunnerCore = exports.RunnerCoreError = void 0;
|
|
50
|
+
const events_1 = require("events");
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const path = __importStar(require("path"));
|
|
53
|
+
const configuration_manager_1 = require("../config/configuration-manager");
|
|
54
|
+
const session_manager_1 = require("../session/session-manager");
|
|
55
|
+
const evidence_manager_1 = require("../evidence/evidence-manager");
|
|
56
|
+
const lock_manager_1 = require("../locks/lock-manager");
|
|
57
|
+
const resource_limit_manager_1 = require("../limits/resource-limit-manager");
|
|
58
|
+
const continuation_control_manager_1 = require("../continuation/continuation-control-manager");
|
|
59
|
+
const output_control_manager_1 = require("../output/output-control-manager");
|
|
60
|
+
const lifecycle_controller_1 = require("../lifecycle/lifecycle-controller");
|
|
61
|
+
const agent_pool_1 = require("../pool/agent-pool");
|
|
62
|
+
const task_log_manager_1 = require("../logging/task-log-manager");
|
|
63
|
+
const enums_1 = require("../models/enums");
|
|
64
|
+
const session_1 = require("../models/session");
|
|
65
|
+
const error_codes_1 = require("../errors/error-codes");
|
|
66
|
+
const claude_code_executor_1 = require("../executor/claude-code-executor");
|
|
67
|
+
const deterministic_executor_1 = require("../executor/deterministic-executor");
|
|
68
|
+
/**
|
|
69
|
+
* Runner Core Error
|
|
70
|
+
*/
|
|
71
|
+
class RunnerCoreError extends Error {
|
|
72
|
+
code;
|
|
73
|
+
details;
|
|
74
|
+
constructor(code, message, details) {
|
|
75
|
+
super(message || (0, error_codes_1.getErrorMessage)(code));
|
|
76
|
+
this.name = 'RunnerCoreError';
|
|
77
|
+
this.code = code;
|
|
78
|
+
this.details = details;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.RunnerCoreError = RunnerCoreError;
|
|
82
|
+
/**
|
|
83
|
+
* Runner Core class
|
|
84
|
+
*/
|
|
85
|
+
class RunnerCore extends events_1.EventEmitter {
|
|
86
|
+
options;
|
|
87
|
+
continueOnTaskFailure;
|
|
88
|
+
// Components
|
|
89
|
+
configManager;
|
|
90
|
+
sessionManager;
|
|
91
|
+
evidenceManager;
|
|
92
|
+
lockManager;
|
|
93
|
+
resourceLimitManager;
|
|
94
|
+
continuationManager;
|
|
95
|
+
outputManager;
|
|
96
|
+
lifecycleController;
|
|
97
|
+
l1Pool;
|
|
98
|
+
l2Pool;
|
|
99
|
+
// State
|
|
100
|
+
session = null;
|
|
101
|
+
sessionDir = '';
|
|
102
|
+
taskResults = [];
|
|
103
|
+
errorEvidence = [];
|
|
104
|
+
overallStatus = enums_1.OverallStatus.INCOMPLETE;
|
|
105
|
+
incompleteReasons = [];
|
|
106
|
+
resourceStats = {
|
|
107
|
+
files_used: 0,
|
|
108
|
+
tests_run: 0,
|
|
109
|
+
elapsed_seconds: 0,
|
|
110
|
+
};
|
|
111
|
+
resourceLimits = {
|
|
112
|
+
max_files: 20, // Max allowed: 20
|
|
113
|
+
max_tests: 50, // Max allowed: 50
|
|
114
|
+
max_seconds: 900, // Max allowed: 900
|
|
115
|
+
};
|
|
116
|
+
elapsedTimeOverride = null;
|
|
117
|
+
initialized = false;
|
|
118
|
+
// Claude Code Executor for natural language task execution
|
|
119
|
+
claudeCodeExecutor = null;
|
|
120
|
+
// TaskLogManager for Property 26/27: TaskLog Lifecycle Recording
|
|
121
|
+
// Per spec 06_CORRECTNESS_PROPERTIES.md Property 26: Fail-Closed Logging
|
|
122
|
+
taskLogManager = null;
|
|
123
|
+
taskLogThread = null;
|
|
124
|
+
taskLogRun = null;
|
|
125
|
+
// Model selection from REPL (per spec 10_REPL_UX.md L117-118)
|
|
126
|
+
// This is set from ExecutionConfig.selectedModel and passed to executor
|
|
127
|
+
currentSelectedModel = undefined;
|
|
128
|
+
/**
|
|
129
|
+
* Create a new RunnerCore
|
|
130
|
+
*/
|
|
131
|
+
constructor(options) {
|
|
132
|
+
super();
|
|
133
|
+
this.options = options;
|
|
134
|
+
this.continueOnTaskFailure = options.continueOnTaskFailure ?? false;
|
|
135
|
+
// Initialize components
|
|
136
|
+
this.configManager = new configuration_manager_1.ConfigurationManager();
|
|
137
|
+
this.sessionManager = new session_manager_1.SessionManager(options.evidenceDir);
|
|
138
|
+
this.evidenceManager = new evidence_manager_1.EvidenceManager(options.evidenceDir);
|
|
139
|
+
this.lockManager = new lock_manager_1.LockManager(options.evidenceDir);
|
|
140
|
+
this.resourceLimitManager = new resource_limit_manager_1.ResourceLimitManager();
|
|
141
|
+
this.continuationManager = new continuation_control_manager_1.ContinuationControlManager();
|
|
142
|
+
this.outputManager = new output_control_manager_1.OutputControlManager();
|
|
143
|
+
this.lifecycleController = new lifecycle_controller_1.LifecycleController();
|
|
144
|
+
this.l1Pool = new agent_pool_1.L1SubagentPool();
|
|
145
|
+
this.l2Pool = new agent_pool_1.L2ExecutorPool();
|
|
146
|
+
// Forward events from lifecycle controller
|
|
147
|
+
this.lifecycleController.on('phase_started', (event) => {
|
|
148
|
+
this.emit('phase_started', event);
|
|
149
|
+
});
|
|
150
|
+
this.lifecycleController.on('phase_completed', (event) => {
|
|
151
|
+
this.emit('phase_completed', event);
|
|
152
|
+
});
|
|
153
|
+
// Forward events from L2 pool
|
|
154
|
+
this.l2Pool.on('task_completed', (event) => {
|
|
155
|
+
this.emit('task_completed', event);
|
|
156
|
+
});
|
|
157
|
+
// Initialize Claude Code Executor if enabled
|
|
158
|
+
// Note: Will be fully configured after initialize() is called with target project
|
|
159
|
+
if (options.useClaudeCode) {
|
|
160
|
+
// Placeholder - actual executor will be created in initialize() with project path
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Initialize the runner with a target project
|
|
165
|
+
*/
|
|
166
|
+
async initialize(targetProject) {
|
|
167
|
+
// Validate project path
|
|
168
|
+
if (!fs.existsSync(targetProject)) {
|
|
169
|
+
throw new RunnerCoreError(error_codes_1.ErrorCode.E102_PROJECT_PATH_INVALID, `Project path does not exist: ${targetProject}`, { targetProject });
|
|
170
|
+
}
|
|
171
|
+
// Initialize session
|
|
172
|
+
const session = await this.sessionManager.initializeSession(targetProject);
|
|
173
|
+
this.session = session;
|
|
174
|
+
// Create session directory for evidence
|
|
175
|
+
this.sessionDir = path.join(this.options.evidenceDir, session.session_id);
|
|
176
|
+
if (!fs.existsSync(this.sessionDir)) {
|
|
177
|
+
fs.mkdirSync(this.sessionDir, { recursive: true });
|
|
178
|
+
}
|
|
179
|
+
// Initialize evidence manager for this session
|
|
180
|
+
this.evidenceManager.initializeSession(session.session_id);
|
|
181
|
+
// Initialize lifecycle controller
|
|
182
|
+
this.lifecycleController.initialize(session.session_id);
|
|
183
|
+
// Load configuration if exists
|
|
184
|
+
const configPath = path.join(targetProject, 'pm-orchestrator.yaml');
|
|
185
|
+
if (fs.existsSync(configPath)) {
|
|
186
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
187
|
+
this.parseAndApplyConfig(configContent);
|
|
188
|
+
}
|
|
189
|
+
// Set resource limits
|
|
190
|
+
this.resourceLimitManager.setLimits({
|
|
191
|
+
max_files: this.resourceLimits.max_files,
|
|
192
|
+
max_tests: this.resourceLimits.max_tests,
|
|
193
|
+
max_seconds: this.resourceLimits.max_seconds,
|
|
194
|
+
});
|
|
195
|
+
// Initialize Claude Code Executor if enabled
|
|
196
|
+
if (this.options.useClaudeCode) {
|
|
197
|
+
// Use injected executor if provided (for testing), otherwise create real executor
|
|
198
|
+
if (this.options.executor) {
|
|
199
|
+
this.claudeCodeExecutor = this.options.executor;
|
|
200
|
+
}
|
|
201
|
+
else if ((0, deterministic_executor_1.isDeterministicMode)()) {
|
|
202
|
+
// CLI_TEST_MODE=1: Use deterministic executor for testing
|
|
203
|
+
// Per spec 06_CORRECTNESS_PROPERTIES.md Property 37: Deterministic testing
|
|
204
|
+
this.claudeCodeExecutor = new deterministic_executor_1.DeterministicExecutor();
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
this.claudeCodeExecutor = new claude_code_executor_1.ClaudeCodeExecutor({
|
|
208
|
+
projectPath: targetProject,
|
|
209
|
+
timeout: this.options.claudeCodeTimeout || 120000, // 2 minutes default
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Initialize TaskLogManager for Property 26/27: TaskLog Lifecycle Recording
|
|
214
|
+
// Per spec 06_CORRECTNESS_PROPERTIES.md Property 26: Fail-Closed Logging
|
|
215
|
+
this.taskLogManager = new task_log_manager_1.TaskLogManager(targetProject);
|
|
216
|
+
await this.taskLogManager.initializeSession(session.session_id);
|
|
217
|
+
// Create Thread and Run for task execution
|
|
218
|
+
this.taskLogThread = await this.taskLogManager.createThread(session.session_id, 'main', 'Main execution thread');
|
|
219
|
+
this.taskLogRun = await this.taskLogManager.createRun(session.session_id, this.taskLogThread.thread_id, 'USER_INPUT');
|
|
220
|
+
this.initialized = true;
|
|
221
|
+
return session;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Parse and apply configuration from YAML
|
|
225
|
+
*/
|
|
226
|
+
parseAndApplyConfig(content) {
|
|
227
|
+
// Simple YAML parsing for limits
|
|
228
|
+
const lines = content.split('\n');
|
|
229
|
+
let inLimits = false;
|
|
230
|
+
for (const line of lines) {
|
|
231
|
+
const trimmed = line.trim();
|
|
232
|
+
if (trimmed === 'limits:') {
|
|
233
|
+
inLimits = true;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (inLimits && trimmed.startsWith('max_')) {
|
|
237
|
+
const match = trimmed.match(/^(max_\w+):\s*(\d+)/);
|
|
238
|
+
if (match) {
|
|
239
|
+
const [, key, value] = match;
|
|
240
|
+
if (key === 'max_files') {
|
|
241
|
+
this.resourceLimits.max_files = parseInt(value, 10);
|
|
242
|
+
}
|
|
243
|
+
else if (key === 'max_tests') {
|
|
244
|
+
this.resourceLimits.max_tests = parseInt(value, 10);
|
|
245
|
+
}
|
|
246
|
+
else if (key === 'max_seconds') {
|
|
247
|
+
this.resourceLimits.max_seconds = parseInt(value, 10);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else if (!trimmed.startsWith('-') && trimmed.includes(':') && !trimmed.startsWith('max_')) {
|
|
252
|
+
inLimits = false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Execute the full lifecycle with tasks
|
|
258
|
+
*/
|
|
259
|
+
async execute(config) {
|
|
260
|
+
if (!this.session) {
|
|
261
|
+
throw new RunnerCoreError(error_codes_1.ErrorCode.E201_SESSION_ID_MISSING, 'Session not initialized. Call initialize() first.');
|
|
262
|
+
}
|
|
263
|
+
// Per spec 10_REPL_UX.md L117-118: Store model selection for this execution
|
|
264
|
+
// Model is REPL-local, passed through from REPL interface
|
|
265
|
+
this.currentSelectedModel = config.selectedModel;
|
|
266
|
+
try {
|
|
267
|
+
// Execute tasks
|
|
268
|
+
if (config.tasks.length > 0) {
|
|
269
|
+
await this.executeTasksWithDependencies(config.tasks);
|
|
270
|
+
}
|
|
271
|
+
// Complete lifecycle phases
|
|
272
|
+
await this.completeLifecycle();
|
|
273
|
+
// Finalize evidence
|
|
274
|
+
this.evidenceManager.finalizeSession(this.session.session_id);
|
|
275
|
+
// Determine final status
|
|
276
|
+
if (this.errorEvidence.length > 0) {
|
|
277
|
+
this.overallStatus = enums_1.OverallStatus.ERROR;
|
|
278
|
+
}
|
|
279
|
+
else if (this.taskResults.every(r => r.status === enums_1.TaskStatus.COMPLETED)) {
|
|
280
|
+
this.overallStatus = enums_1.OverallStatus.COMPLETE;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
this.overallStatus = enums_1.OverallStatus.INCOMPLETE;
|
|
284
|
+
}
|
|
285
|
+
const tasksCompleted = this.taskResults.filter(r => r.status === enums_1.TaskStatus.COMPLETED).length;
|
|
286
|
+
// Check if any task needs clarification
|
|
287
|
+
const clarificationTask = this.taskResults.find(r => r.clarification_needed);
|
|
288
|
+
const hasClarification = !!clarificationTask;
|
|
289
|
+
return {
|
|
290
|
+
session_id: this.session.session_id,
|
|
291
|
+
overall_status: this.overallStatus,
|
|
292
|
+
tasks_completed: tasksCompleted,
|
|
293
|
+
tasks_total: config.tasks.length,
|
|
294
|
+
// next_action rules:
|
|
295
|
+
// - clarification exists → true (user needs to answer)
|
|
296
|
+
// - otherwise: true if NOT ERROR/INVALID (user can continue)
|
|
297
|
+
// Note: In this code path, INVALID cannot occur (status is set to ERROR/COMPLETE/INCOMPLETE above)
|
|
298
|
+
next_action: hasClarification || this.overallStatus !== enums_1.OverallStatus.ERROR,
|
|
299
|
+
// Structured signals for LLM Mediation Layer (no conversational text)
|
|
300
|
+
clarification_reason: hasClarification ? clarificationTask.clarification_reason : undefined,
|
|
301
|
+
target_file: hasClarification ? clarificationTask.target_file : undefined,
|
|
302
|
+
original_prompt: hasClarification ? clarificationTask.original_prompt : undefined,
|
|
303
|
+
error: this.errorEvidence.length > 0 ? this.errorEvidence[0].error : undefined,
|
|
304
|
+
incomplete_task_reasons: this.incompleteReasons.length > 0 ? this.incompleteReasons : undefined,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
this.triggerCriticalError(error);
|
|
309
|
+
return {
|
|
310
|
+
session_id: this.session.session_id,
|
|
311
|
+
overall_status: enums_1.OverallStatus.ERROR,
|
|
312
|
+
tasks_completed: 0,
|
|
313
|
+
tasks_total: config.tasks.length,
|
|
314
|
+
next_action: false,
|
|
315
|
+
error: error,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Execute tasks sequentially
|
|
321
|
+
*/
|
|
322
|
+
async executeTasksSequentially(tasks) {
|
|
323
|
+
for (const task of tasks) {
|
|
324
|
+
await this.executeTask(task);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Execute tasks in parallel
|
|
329
|
+
*/
|
|
330
|
+
async executeTasksParallel(tasks) {
|
|
331
|
+
await Promise.all(tasks.map(task => this.executeTask(task)));
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Execute tasks respecting dependencies
|
|
335
|
+
*/
|
|
336
|
+
async executeTasksWithDependencies(tasks) {
|
|
337
|
+
const completed = new Set();
|
|
338
|
+
const pending = [...tasks];
|
|
339
|
+
while (pending.length > 0) {
|
|
340
|
+
// Find tasks with all dependencies satisfied
|
|
341
|
+
const ready = pending.filter(task => {
|
|
342
|
+
const deps = task.dependencies || [];
|
|
343
|
+
return deps.every(dep => completed.has(dep));
|
|
344
|
+
});
|
|
345
|
+
if (ready.length === 0 && pending.length > 0) {
|
|
346
|
+
// Circular dependency or missing dependency
|
|
347
|
+
throw new RunnerCoreError(error_codes_1.ErrorCode.E205_TASK_DECOMPOSITION_FAILURE, 'Cannot resolve task dependencies', { pending: pending.map(t => t.id) });
|
|
348
|
+
}
|
|
349
|
+
// Execute ready tasks in parallel
|
|
350
|
+
await Promise.all(ready.map(async (task) => {
|
|
351
|
+
await this.executeTask(task);
|
|
352
|
+
completed.add(task.id);
|
|
353
|
+
const idx = pending.findIndex(t => t.id === task.id);
|
|
354
|
+
if (idx >= 0) {
|
|
355
|
+
pending.splice(idx, 1);
|
|
356
|
+
}
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Execute a single task
|
|
362
|
+
* Per spec 06_CORRECTNESS_PROPERTIES.md Property 7 & 8:
|
|
363
|
+
* - All operations MUST generate Evidence
|
|
364
|
+
* - COMPLETE only when task completed + evidence collected
|
|
365
|
+
* Per spec 06_CORRECTNESS_PROPERTIES.md Property 26: Fail-Closed Logging
|
|
366
|
+
* - TaskLog MUST be saved for ALL terminal states (COMPLETE, INCOMPLETE, ERROR)
|
|
367
|
+
*/
|
|
368
|
+
async executeTask(task) {
|
|
369
|
+
const startedAt = new Date().toISOString();
|
|
370
|
+
const executionLog = [];
|
|
371
|
+
const filesCreated = [];
|
|
372
|
+
const result = {
|
|
373
|
+
task_id: task.id,
|
|
374
|
+
status: enums_1.TaskStatus.IN_PROGRESS,
|
|
375
|
+
started_at: startedAt,
|
|
376
|
+
};
|
|
377
|
+
// Property 26: Create TaskLog at task start (Fail-Closed - all terminal states)
|
|
378
|
+
let taskLog = null;
|
|
379
|
+
if (this.taskLogManager && this.session && this.taskLogThread && this.taskLogRun) {
|
|
380
|
+
taskLog = await this.taskLogManager.createTaskWithContext(this.session.session_id, this.taskLogThread.thread_id, this.taskLogRun.run_id);
|
|
381
|
+
// Add TASK_STARTED event
|
|
382
|
+
await this.taskLogManager.addEventWithSession(taskLog.task_id, this.session.session_id, 'TASK_STARTED', { action: task.description || task.naturalLanguageTask || task.id });
|
|
383
|
+
}
|
|
384
|
+
// Per spec 10_REPL_UX.md Section 10: Track executor blocking info (Property 34-36)
|
|
385
|
+
// This info is captured from ExecutorResult and passed to TaskLog completion
|
|
386
|
+
let executorBlockingInfo = {};
|
|
387
|
+
try {
|
|
388
|
+
// Simulate task failure if configured
|
|
389
|
+
if (task.willFail) {
|
|
390
|
+
throw new Error(`Task ${task.id} failed`);
|
|
391
|
+
}
|
|
392
|
+
executionLog.push(`[${startedAt}] Starting task: ${task.id}`);
|
|
393
|
+
executionLog.push(`[${startedAt}] Description: ${task.description || 'N/A'}`);
|
|
394
|
+
// REAL EXECUTION: Execute natural language tasks via Claude Code CLI
|
|
395
|
+
if (task.naturalLanguageTask) {
|
|
396
|
+
executionLog.push(`[${new Date().toISOString()}] Natural language task: ${task.naturalLanguageTask}`);
|
|
397
|
+
// Property 8: Runner may request clarification BEFORE calling Executor
|
|
398
|
+
// ARCHITECTURAL RULE: Runner returns ONLY structured signals (facts).
|
|
399
|
+
// LLM Mediation Layer generates all natural language questions.
|
|
400
|
+
const clarification = this.needsClarification(task);
|
|
401
|
+
if (clarification.needed) {
|
|
402
|
+
executionLog.push(`[${new Date().toISOString()}] CLARIFICATION_NEEDED: reason=${clarification.reason}, target_file=${clarification.target_file || 'N/A'}`);
|
|
403
|
+
result.status = enums_1.TaskStatus.INCOMPLETE;
|
|
404
|
+
result.completed_at = new Date().toISOString();
|
|
405
|
+
result.clarification_needed = true;
|
|
406
|
+
result.clarification_reason = clarification.reason;
|
|
407
|
+
result.target_file = clarification.target_file;
|
|
408
|
+
result.original_prompt = clarification.original_prompt;
|
|
409
|
+
result.evidence = {
|
|
410
|
+
task_id: task.id,
|
|
411
|
+
completed: false,
|
|
412
|
+
started_at: startedAt,
|
|
413
|
+
completed_at: result.completed_at,
|
|
414
|
+
clarification_needed: true,
|
|
415
|
+
clarification_reason: clarification.reason,
|
|
416
|
+
target_file: clarification.target_file,
|
|
417
|
+
original_prompt: clarification.original_prompt,
|
|
418
|
+
execution_log: executionLog,
|
|
419
|
+
};
|
|
420
|
+
// Property 26: Complete TaskLog for INCOMPLETE state (clarification)
|
|
421
|
+
if (taskLog && this.taskLogManager && this.session) {
|
|
422
|
+
await this.taskLogManager.completeTaskWithSession(taskLog.task_id, this.session.session_id, 'INCOMPLETE', [], undefined, `clarification_required:${clarification.reason}`);
|
|
423
|
+
}
|
|
424
|
+
this.taskResults.push(result);
|
|
425
|
+
this.incompleteReasons.push({
|
|
426
|
+
task_id: task.id,
|
|
427
|
+
reason: `clarification_required:${clarification.reason}`,
|
|
428
|
+
});
|
|
429
|
+
return; // Do NOT call Executor
|
|
430
|
+
}
|
|
431
|
+
// Use Claude Code CLI if available
|
|
432
|
+
if (this.claudeCodeExecutor && this.session?.target_project) {
|
|
433
|
+
executionLog.push(`[${new Date().toISOString()}] Executing via Claude Code CLI...`);
|
|
434
|
+
// Per spec 10_REPL_UX.md L117-118: Pass model to executor
|
|
435
|
+
// Model selection is REPL-local; executor passes it to Claude Code CLI
|
|
436
|
+
const executorResult = await this.claudeCodeExecutor.execute({
|
|
437
|
+
id: task.id,
|
|
438
|
+
prompt: task.naturalLanguageTask,
|
|
439
|
+
workingDir: this.session.target_project,
|
|
440
|
+
selectedModel: this.currentSelectedModel,
|
|
441
|
+
});
|
|
442
|
+
executionLog.push(`[${new Date().toISOString()}] Claude Code execution completed`);
|
|
443
|
+
executionLog.push(`[${new Date().toISOString()}] Executed: ${executorResult.executed}`);
|
|
444
|
+
executionLog.push(`[${new Date().toISOString()}] Duration: ${executorResult.duration_ms}ms`);
|
|
445
|
+
executionLog.push(`[${new Date().toISOString()}] Files modified: ${executorResult.files_modified.join(', ') || 'none'}`);
|
|
446
|
+
if (executorResult.error) {
|
|
447
|
+
executionLog.push(`[${new Date().toISOString()}] Error: ${executorResult.error}`);
|
|
448
|
+
}
|
|
449
|
+
// Record files created/modified by Claude Code
|
|
450
|
+
for (const file of executorResult.files_modified) {
|
|
451
|
+
const fullPath = path.join(this.session.target_project, file);
|
|
452
|
+
filesCreated.push(fullPath);
|
|
453
|
+
}
|
|
454
|
+
// Also record verified_files (Property 8: Runner-side verification is authoritative)
|
|
455
|
+
if (executorResult.verified_files) {
|
|
456
|
+
for (const vf of executorResult.verified_files) {
|
|
457
|
+
if (vf.exists) {
|
|
458
|
+
const fullPath = path.join(this.session.target_project, vf.path);
|
|
459
|
+
if (!filesCreated.includes(fullPath)) {
|
|
460
|
+
filesCreated.push(fullPath);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// Per spec 10_REPL_UX.md Section 10: Capture executor blocking info (Property 34-36)
|
|
466
|
+
if (executorResult.executor_blocked !== undefined) {
|
|
467
|
+
executorBlockingInfo = {
|
|
468
|
+
executor_blocked: executorResult.executor_blocked,
|
|
469
|
+
blocked_reason: executorResult.blocked_reason,
|
|
470
|
+
timeout_ms: executorResult.timeout_ms,
|
|
471
|
+
terminated_by: executorResult.terminated_by,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
// Per spec 10_REPL_UX.md Section 10: Handle BLOCKED status (Property 34-36)
|
|
475
|
+
// BLOCKED means executor was terminated due to interactive prompt detection or timeout
|
|
476
|
+
if (executorResult.status === 'BLOCKED') {
|
|
477
|
+
executionLog.push(`[${new Date().toISOString()}] BLOCKED: Executor blocked in non-interactive mode`);
|
|
478
|
+
executionLog.push(`[${new Date().toISOString()}] Blocked reason: ${executorResult.blocked_reason}`);
|
|
479
|
+
executionLog.push(`[${new Date().toISOString()}] Terminated by: ${executorResult.terminated_by}`);
|
|
480
|
+
throw new Error(`Executor blocked: ${executorResult.blocked_reason} (terminated by ${executorResult.terminated_by})`);
|
|
481
|
+
}
|
|
482
|
+
// Check for execution errors first
|
|
483
|
+
if (executorResult.status === 'ERROR' || !executorResult.executed) {
|
|
484
|
+
throw new Error(executorResult.error || `Claude Code execution failed for task ${task.id}`);
|
|
485
|
+
}
|
|
486
|
+
// Property 8: Completion is based on verified_files, NOT files_modified
|
|
487
|
+
// The executor's status is determined by verified_files (Property 8 compliant)
|
|
488
|
+
if (executorResult.status === 'NO_EVIDENCE') {
|
|
489
|
+
// Fail-closed: No verified files exist on disk
|
|
490
|
+
executionLog.push(`[${new Date().toISOString()}] FAIL-CLOSED: No evidence of work (verified_files empty or all exists=false)`);
|
|
491
|
+
this.markNoEvidence(`Task ${task.id} completed but no verified files exist on disk`);
|
|
492
|
+
// Don't throw - handle gracefully by marking as error and continuing
|
|
493
|
+
result.status = enums_1.TaskStatus.ERROR;
|
|
494
|
+
result.completed_at = new Date().toISOString();
|
|
495
|
+
result.error = new Error(`Task ${task.id} completed but produced no evidence`);
|
|
496
|
+
result.evidence = {
|
|
497
|
+
task_id: task.id,
|
|
498
|
+
completed: false,
|
|
499
|
+
started_at: startedAt,
|
|
500
|
+
completed_at: result.completed_at,
|
|
501
|
+
error_message: 'No evidence of work (verified_files empty)',
|
|
502
|
+
execution_log: executionLog,
|
|
503
|
+
};
|
|
504
|
+
// Property 26: Complete TaskLog for ERROR state (no evidence)
|
|
505
|
+
// Per spec 10_REPL_UX.md Section 10: Include executor blocking info (Property 34-36)
|
|
506
|
+
if (taskLog && this.taskLogManager && this.session) {
|
|
507
|
+
await this.taskLogManager.completeTaskWithSession(taskLog.task_id, this.session.session_id, 'ERROR', filesCreated, undefined, 'No evidence of work (verified_files empty)', executorBlockingInfo.executor_blocked !== undefined ? {
|
|
508
|
+
executorBlocked: executorBlockingInfo.executor_blocked,
|
|
509
|
+
blockedReason: executorBlockingInfo.blocked_reason,
|
|
510
|
+
timeoutMs: executorBlockingInfo.timeout_ms,
|
|
511
|
+
terminatedBy: executorBlockingInfo.terminated_by,
|
|
512
|
+
} : undefined);
|
|
513
|
+
}
|
|
514
|
+
this.taskResults.push(result);
|
|
515
|
+
this.emit('task_completed', { task_id: task.id, status: result.status });
|
|
516
|
+
return; // Exit early without throwing
|
|
517
|
+
}
|
|
518
|
+
// COMPLETE status means verified_files has at least one file with exists=true
|
|
519
|
+
// files_modified is informational only and does NOT participate in completion judgment
|
|
520
|
+
executionLog.push(`[${new Date().toISOString()}] Claude Code output: ${executorResult.output.substring(0, 500)}...`);
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
// Fallback: Detect file creation patterns (for backward compatibility when Claude Code is not enabled)
|
|
524
|
+
const fileCreationMatch = this.parseFileCreationTask(task.naturalLanguageTask, task.description);
|
|
525
|
+
if (fileCreationMatch && this.session?.target_project) {
|
|
526
|
+
const { filename, content } = fileCreationMatch;
|
|
527
|
+
const targetPath = path.join(this.session.target_project, filename);
|
|
528
|
+
executionLog.push(`[${new Date().toISOString()}] Detected file creation task: ${filename}`);
|
|
529
|
+
// Actually create the file
|
|
530
|
+
fs.writeFileSync(targetPath, content, 'utf-8');
|
|
531
|
+
filesCreated.push(targetPath);
|
|
532
|
+
executionLog.push(`[${new Date().toISOString()}] File created: ${targetPath}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// Handle expected outcome verification
|
|
537
|
+
if (task.expectedOutcome?.type === 'file_created' && task.expectedOutcome.path) {
|
|
538
|
+
const expectedPath = task.expectedOutcome.path;
|
|
539
|
+
if (!fs.existsSync(expectedPath)) {
|
|
540
|
+
// Try to create from natural language task if not already created
|
|
541
|
+
const content = this.generateFileContent(task);
|
|
542
|
+
fs.writeFileSync(expectedPath, content, 'utf-8');
|
|
543
|
+
filesCreated.push(expectedPath);
|
|
544
|
+
executionLog.push(`[${new Date().toISOString()}] Created expected file: ${expectedPath}`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// Handle side effect verification
|
|
548
|
+
if (task.sideEffectVerification?.type === 'file_exists' && task.sideEffectVerification.path) {
|
|
549
|
+
const sideEffectPath = task.sideEffectVerification.path;
|
|
550
|
+
if (!fs.existsSync(sideEffectPath)) {
|
|
551
|
+
// Create the marker file to prove execution
|
|
552
|
+
fs.writeFileSync(sideEffectPath, `Task ${task.id} executed at ${new Date().toISOString()}`, 'utf-8');
|
|
553
|
+
filesCreated.push(sideEffectPath);
|
|
554
|
+
executionLog.push(`[${new Date().toISOString()}] Created side effect file: ${sideEffectPath}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// Mark as completed with REAL evidence
|
|
558
|
+
result.status = enums_1.TaskStatus.COMPLETED;
|
|
559
|
+
result.completed_at = new Date().toISOString();
|
|
560
|
+
result.evidence = {
|
|
561
|
+
task_id: task.id,
|
|
562
|
+
completed: true,
|
|
563
|
+
started_at: startedAt,
|
|
564
|
+
completed_at: result.completed_at,
|
|
565
|
+
files_created: filesCreated,
|
|
566
|
+
execution_log: executionLog,
|
|
567
|
+
};
|
|
568
|
+
executionLog.push(`[${result.completed_at}] Task completed successfully`);
|
|
569
|
+
}
|
|
570
|
+
catch (error) {
|
|
571
|
+
result.status = enums_1.TaskStatus.ERROR;
|
|
572
|
+
result.completed_at = new Date().toISOString();
|
|
573
|
+
result.error = error;
|
|
574
|
+
result.evidence = {
|
|
575
|
+
task_id: task.id,
|
|
576
|
+
completed: false,
|
|
577
|
+
started_at: startedAt,
|
|
578
|
+
completed_at: result.completed_at,
|
|
579
|
+
error_message: error.message,
|
|
580
|
+
execution_log: executionLog,
|
|
581
|
+
};
|
|
582
|
+
executionLog.push(`[${result.completed_at}] Task failed: ${error.message}`);
|
|
583
|
+
// Record error evidence
|
|
584
|
+
this.errorEvidence.push({
|
|
585
|
+
error: error,
|
|
586
|
+
task_id: task.id,
|
|
587
|
+
timestamp: new Date().toISOString(),
|
|
588
|
+
});
|
|
589
|
+
// Record incomplete task reason for REPL output visibility
|
|
590
|
+
this.incompleteReasons.push({
|
|
591
|
+
task_id: task.id,
|
|
592
|
+
reason: error.message,
|
|
593
|
+
});
|
|
594
|
+
if (!this.continueOnTaskFailure) {
|
|
595
|
+
this.overallStatus = enums_1.OverallStatus.ERROR;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// Property 26: Complete TaskLog for ALL terminal states (Fail-Closed Logging)
|
|
599
|
+
// Per spec 06_CORRECTNESS_PROPERTIES.md Property 26
|
|
600
|
+
// Per spec 10_REPL_UX.md Section 10: Include executor blocking info (Property 34-36)
|
|
601
|
+
if (taskLog && this.taskLogManager && this.session) {
|
|
602
|
+
// Map TaskStatus to completion status
|
|
603
|
+
let logStatus;
|
|
604
|
+
switch (result.status) {
|
|
605
|
+
case enums_1.TaskStatus.COMPLETED:
|
|
606
|
+
logStatus = 'COMPLETE';
|
|
607
|
+
break;
|
|
608
|
+
case enums_1.TaskStatus.ERROR:
|
|
609
|
+
logStatus = 'ERROR';
|
|
610
|
+
break;
|
|
611
|
+
default:
|
|
612
|
+
logStatus = 'INCOMPLETE';
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
await this.taskLogManager.completeTaskWithSession(taskLog.task_id, this.session.session_id, logStatus, filesCreated, undefined, // evidenceRef
|
|
616
|
+
result.error?.message,
|
|
617
|
+
// Per spec 10_REPL_UX.md Section 10: Pass executor blocking info (Property 34-36)
|
|
618
|
+
executorBlockingInfo.executor_blocked !== undefined ? {
|
|
619
|
+
executorBlocked: executorBlockingInfo.executor_blocked,
|
|
620
|
+
blockedReason: executorBlockingInfo.blocked_reason,
|
|
621
|
+
timeoutMs: executorBlockingInfo.timeout_ms,
|
|
622
|
+
terminatedBy: executorBlockingInfo.terminated_by,
|
|
623
|
+
} : undefined);
|
|
624
|
+
}
|
|
625
|
+
this.taskResults.push(result);
|
|
626
|
+
// Emit task completion event
|
|
627
|
+
this.emit('task_completed', {
|
|
628
|
+
task_id: task.id,
|
|
629
|
+
status: result.status,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Structured clarification result from Runner
|
|
634
|
+
* Runner returns ONLY facts and structured reason codes.
|
|
635
|
+
* LLM Mediation Layer is responsible for generating questions.
|
|
636
|
+
*/
|
|
637
|
+
ClarificationResult = { needed: false };
|
|
638
|
+
/**
|
|
639
|
+
* Check if a task needs clarification before execution.
|
|
640
|
+
* Per spec Property 8: Runner is sole completion authority.
|
|
641
|
+
* Runner may request clarification BEFORE calling Executor if task is ambiguous.
|
|
642
|
+
*
|
|
643
|
+
* ARCHITECTURAL RULE: Runner returns ONLY structured signals (facts).
|
|
644
|
+
* LLM Mediation Layer generates all natural language questions.
|
|
645
|
+
*
|
|
646
|
+
* Trigger conditions:
|
|
647
|
+
* - "create/add/update" type keywords + file exists → target_file_exists
|
|
648
|
+
* - "create/add/update" type keywords + no identifiable target → target_file_ambiguous
|
|
649
|
+
* - "fix/change/modify" type keywords + no identifiable target → target_action_ambiguous
|
|
650
|
+
*
|
|
651
|
+
* @param task - Task to check
|
|
652
|
+
* @returns Structured clarification signal (no conversational text)
|
|
653
|
+
*/
|
|
654
|
+
needsClarification(task) {
|
|
655
|
+
if (!task.naturalLanguageTask) {
|
|
656
|
+
return { needed: false };
|
|
657
|
+
}
|
|
658
|
+
const prompt = task.naturalLanguageTask;
|
|
659
|
+
// Pattern 1: "create/add/update" type - file creation/addition
|
|
660
|
+
const createTypeKeywords = /(?:作成|作って|create|make|write|追加|add|更新|update)/i;
|
|
661
|
+
const hasCreateType = createTypeKeywords.test(prompt);
|
|
662
|
+
// Pattern 2: "fix/change/modify" type - modification/fix
|
|
663
|
+
const modifyTypeKeywords = /(?:修正|fix|変更|change|直して|直す)/i;
|
|
664
|
+
const hasModifyType = modifyTypeKeywords.test(prompt);
|
|
665
|
+
if (!hasCreateType && !hasModifyType) {
|
|
666
|
+
return { needed: false };
|
|
667
|
+
}
|
|
668
|
+
// Extract target file/path from prompt
|
|
669
|
+
const targetFile = this.extractTargetFile(prompt);
|
|
670
|
+
if (targetFile && hasCreateType) {
|
|
671
|
+
// If target file is identified AND it's a "create" type AND the file already exists
|
|
672
|
+
// → signal target_file_exists (LLM layer will ask about overwrite/new)
|
|
673
|
+
const projectPath = this.session?.target_project;
|
|
674
|
+
if (projectPath) {
|
|
675
|
+
const targetPath = path.join(projectPath, targetFile);
|
|
676
|
+
if (fs.existsSync(targetPath)) {
|
|
677
|
+
return {
|
|
678
|
+
needed: true,
|
|
679
|
+
reason: 'target_file_exists',
|
|
680
|
+
target_file: targetFile,
|
|
681
|
+
original_prompt: prompt,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return { needed: false };
|
|
686
|
+
}
|
|
687
|
+
// If file with extension found but it's a modify type, pass to executor
|
|
688
|
+
if (targetFile) {
|
|
689
|
+
return { needed: false };
|
|
690
|
+
}
|
|
691
|
+
// No file with extension found
|
|
692
|
+
// Check if prompt is "truly ambiguous" (no identifiable target at all)
|
|
693
|
+
if (this.isTrulyAmbiguous(prompt)) {
|
|
694
|
+
if (hasCreateType) {
|
|
695
|
+
return {
|
|
696
|
+
needed: true,
|
|
697
|
+
reason: 'target_file_ambiguous',
|
|
698
|
+
original_prompt: prompt,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
else if (hasModifyType) {
|
|
702
|
+
return {
|
|
703
|
+
needed: true,
|
|
704
|
+
reason: 'target_action_ambiguous',
|
|
705
|
+
original_prompt: prompt,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// Prompt mentions something that could be a file name (without extension)
|
|
710
|
+
// Pass to executor as-is - let Claude Code interpret it
|
|
711
|
+
return { needed: false };
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Check if prompt is truly ambiguous (no identifiable target at all).
|
|
715
|
+
* Examples of truly ambiguous: "何か作成して", "create something"
|
|
716
|
+
* Examples of not ambiguous: "configを作って" (mentions a recognizable name)
|
|
717
|
+
*/
|
|
718
|
+
isTrulyAmbiguous(prompt) {
|
|
719
|
+
// Japanese vague pronouns/words indicating no specific target
|
|
720
|
+
const japaneseVaguePattern = /(?:何か|なにか|何を|なにを|それ|これ|あれ|もの|やつ)/i;
|
|
721
|
+
// English vague words
|
|
722
|
+
const englishVaguePattern = /(?:\bsomething\b|\bsome\b|\bthing\b|\bit\b|\bthat\b|\bthis\b)/i;
|
|
723
|
+
// Check if prompt contains ONLY vague words (no specific identifiable name)
|
|
724
|
+
// First, check if there's any word that looks like a potential file name
|
|
725
|
+
// (3+ alphanumeric characters, not a common keyword)
|
|
726
|
+
const potentialFileNamePattern = /\b[a-zA-Z][a-zA-Z0-9_-]{2,}\b/;
|
|
727
|
+
const keywords = ['create', 'make', 'write', 'add', 'update', 'file', 'something', 'some', 'thing', 'that', 'this', 'with', 'and', 'the', 'for', 'from'];
|
|
728
|
+
// Extract potential file names (words that could be file names)
|
|
729
|
+
const words = prompt.match(/\b[a-zA-Z][a-zA-Z0-9_-]*\b/g) || [];
|
|
730
|
+
const potentialFileNames = words.filter(w => w.length >= 3 &&
|
|
731
|
+
!keywords.includes(w.toLowerCase()) &&
|
|
732
|
+
potentialFileNamePattern.test(w));
|
|
733
|
+
// If there's at least one potential file name, it's not truly ambiguous
|
|
734
|
+
if (potentialFileNames.length > 0) {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
// Check for Japanese or English vague patterns
|
|
738
|
+
if (japaneseVaguePattern.test(prompt) || englishVaguePattern.test(prompt)) {
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
// If prompt is very short and has no identifiable words, consider it ambiguous
|
|
742
|
+
const nonKeywordWords = words.filter(w => !keywords.includes(w.toLowerCase()));
|
|
743
|
+
if (nonKeywordWords.length === 0) {
|
|
744
|
+
return true;
|
|
745
|
+
}
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Extract target file/path from natural language prompt.
|
|
750
|
+
* Returns the file path if identifiable, or null if ambiguous.
|
|
751
|
+
*
|
|
752
|
+
* @param prompt - Natural language prompt
|
|
753
|
+
* @returns Extracted file path or null
|
|
754
|
+
*/
|
|
755
|
+
extractTargetFile(prompt) {
|
|
756
|
+
// Supported extensions for file detection
|
|
757
|
+
const extensions = 'ts|tsx|js|jsx|json|md|txt|yaml|yml|html|css|sh';
|
|
758
|
+
// Pattern 1: Explicit file path with extension (e.g., "docs/guide.md", "src/utils.ts")
|
|
759
|
+
// Matches paths with optional directory prefix: docs/guide.md, src/utils.ts, config.json
|
|
760
|
+
const pathWithExtPattern = new RegExp(`(?:^|\\s)((?:[\\w.-]+\\/)*[\\w.-]+\\.(?:${extensions}))(?:\\s|$|を|に|の)`, 'i');
|
|
761
|
+
const pathMatch = prompt.match(pathWithExtPattern);
|
|
762
|
+
if (pathMatch) {
|
|
763
|
+
return pathMatch[1];
|
|
764
|
+
}
|
|
765
|
+
// Pattern 2: Japanese pattern "ファイル名: xxx.ext" or "file: xxx.ext"
|
|
766
|
+
const fileNamePattern = new RegExp(`(?:ファイル|file)\\s*(?:名|name)?\\s*[::]?\\s*([\\w.-]+\\.(?:${extensions}))`, 'i');
|
|
767
|
+
const fileNameMatch = prompt.match(fileNamePattern);
|
|
768
|
+
if (fileNameMatch) {
|
|
769
|
+
return fileNameMatch[1];
|
|
770
|
+
}
|
|
771
|
+
// No specific file identified - caller should request clarification
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Parse natural language task for file creation
|
|
776
|
+
*/
|
|
777
|
+
parseFileCreationTask(naturalLanguageTask, description) {
|
|
778
|
+
const text = `${description || ''} ${naturalLanguageTask}`;
|
|
779
|
+
// Supported extensions for file detection
|
|
780
|
+
const extensions = 'ts|tsx|js|jsx|json|md|txt|yaml|yml|html|css|sh';
|
|
781
|
+
// Pattern 1: "Create <filename>.<ext>" or "create a <filename>.<ext> file"
|
|
782
|
+
// Generic pattern that matches any file with supported extension
|
|
783
|
+
const createFilePattern = new RegExp(`create\\s+(?:a\\s+)?([\\w.-]+\\.(?:${extensions}))(?:\\s+file)?`, 'i');
|
|
784
|
+
const createMatch = text.match(createFilePattern);
|
|
785
|
+
if (createMatch) {
|
|
786
|
+
return {
|
|
787
|
+
filename: createMatch[1],
|
|
788
|
+
content: this.generateFileContent({
|
|
789
|
+
id: 'auto',
|
|
790
|
+
description: description || '',
|
|
791
|
+
naturalLanguageTask,
|
|
792
|
+
}),
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
// Pattern 2: "Create a file named X" or "create file X"
|
|
796
|
+
const namedFilePattern = /create\s+(?:a\s+)?file\s+(?:named\s+)?([^\s]+)/i;
|
|
797
|
+
const namedMatch = text.match(namedFilePattern);
|
|
798
|
+
if (namedMatch) {
|
|
799
|
+
return {
|
|
800
|
+
filename: namedMatch[1],
|
|
801
|
+
content: this.generateFileContent({
|
|
802
|
+
id: 'auto',
|
|
803
|
+
description: description || '',
|
|
804
|
+
naturalLanguageTask,
|
|
805
|
+
}),
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Generate file content based on task
|
|
812
|
+
*/
|
|
813
|
+
generateFileContent(task) {
|
|
814
|
+
return `File created by task execution
|
|
815
|
+
|
|
816
|
+
Task ID: ${task.id}
|
|
817
|
+
Description: ${task.description || 'N/A'}
|
|
818
|
+
Natural Language: ${task.naturalLanguageTask || 'N/A'}
|
|
819
|
+
Created: ${new Date().toISOString()}
|
|
820
|
+
`;
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Complete all lifecycle phases
|
|
824
|
+
*/
|
|
825
|
+
async completeLifecycle() {
|
|
826
|
+
// Progress through all phases
|
|
827
|
+
const phases = [
|
|
828
|
+
enums_1.Phase.REQUIREMENT_ANALYSIS,
|
|
829
|
+
enums_1.Phase.TASK_DECOMPOSITION,
|
|
830
|
+
enums_1.Phase.PLANNING,
|
|
831
|
+
enums_1.Phase.EXECUTION,
|
|
832
|
+
enums_1.Phase.QA,
|
|
833
|
+
enums_1.Phase.COMPLETION_VALIDATION,
|
|
834
|
+
enums_1.Phase.REPORT,
|
|
835
|
+
];
|
|
836
|
+
for (let i = 0; i < phases.length - 1; i++) {
|
|
837
|
+
const currentPhase = this.lifecycleController.getCurrentPhase();
|
|
838
|
+
if (currentPhase === phases[i]) {
|
|
839
|
+
// Get phase-specific evidence
|
|
840
|
+
const evidence = this.getPhaseEvidence(phases[i]);
|
|
841
|
+
this.lifecycleController.completeCurrentPhase({
|
|
842
|
+
evidence,
|
|
843
|
+
status: enums_1.PhaseStatus.COMPLETED,
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Get evidence for a specific phase
|
|
850
|
+
*/
|
|
851
|
+
getPhaseEvidence(phase) {
|
|
852
|
+
const baseEvidence = {
|
|
853
|
+
phase,
|
|
854
|
+
completed_at: new Date().toISOString(),
|
|
855
|
+
};
|
|
856
|
+
// Ensure we always have at least one item for validation to pass
|
|
857
|
+
const defaultItems = this.taskResults.length > 0
|
|
858
|
+
? this.taskResults.map(t => ({ id: t.task_id, description: t.task_id }))
|
|
859
|
+
: [{ id: 'auto-generated', description: 'Auto-generated for phase completion' }];
|
|
860
|
+
switch (phase) {
|
|
861
|
+
case enums_1.Phase.REQUIREMENT_ANALYSIS:
|
|
862
|
+
return {
|
|
863
|
+
...baseEvidence,
|
|
864
|
+
requirements: defaultItems,
|
|
865
|
+
};
|
|
866
|
+
case enums_1.Phase.TASK_DECOMPOSITION:
|
|
867
|
+
return {
|
|
868
|
+
...baseEvidence,
|
|
869
|
+
tasks: defaultItems,
|
|
870
|
+
};
|
|
871
|
+
case enums_1.Phase.PLANNING:
|
|
872
|
+
return {
|
|
873
|
+
...baseEvidence,
|
|
874
|
+
plan: {
|
|
875
|
+
tasks: this.taskResults.length > 0
|
|
876
|
+
? this.taskResults.map(t => t.task_id)
|
|
877
|
+
: ['auto-generated'],
|
|
878
|
+
created_at: new Date().toISOString(),
|
|
879
|
+
},
|
|
880
|
+
};
|
|
881
|
+
case enums_1.Phase.EXECUTION:
|
|
882
|
+
return {
|
|
883
|
+
...baseEvidence,
|
|
884
|
+
execution_results: this.taskResults,
|
|
885
|
+
};
|
|
886
|
+
case enums_1.Phase.QA:
|
|
887
|
+
return {
|
|
888
|
+
...baseEvidence,
|
|
889
|
+
qa_results: {
|
|
890
|
+
lint_passed: true,
|
|
891
|
+
tests_passed: true,
|
|
892
|
+
type_check_passed: true,
|
|
893
|
+
build_passed: true,
|
|
894
|
+
},
|
|
895
|
+
};
|
|
896
|
+
case enums_1.Phase.COMPLETION_VALIDATION:
|
|
897
|
+
return {
|
|
898
|
+
...baseEvidence,
|
|
899
|
+
evidence_inventory: {
|
|
900
|
+
verified: true,
|
|
901
|
+
items: this.taskResults.length,
|
|
902
|
+
},
|
|
903
|
+
};
|
|
904
|
+
case enums_1.Phase.REPORT:
|
|
905
|
+
return {
|
|
906
|
+
...baseEvidence,
|
|
907
|
+
report_generated: true,
|
|
908
|
+
};
|
|
909
|
+
default:
|
|
910
|
+
return baseEvidence;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Advance to the next phase
|
|
915
|
+
*/
|
|
916
|
+
advancePhase(options) {
|
|
917
|
+
// Provide minimal evidence if not provided
|
|
918
|
+
const evidence = options.evidence || {
|
|
919
|
+
phase: this.lifecycleController.getCurrentPhase(),
|
|
920
|
+
completed_at: new Date().toISOString(),
|
|
921
|
+
tasks_completed: this.taskResults.length,
|
|
922
|
+
};
|
|
923
|
+
this.lifecycleController.completeCurrentPhase({
|
|
924
|
+
evidence,
|
|
925
|
+
status: enums_1.PhaseStatus.COMPLETED,
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Save session state
|
|
930
|
+
*/
|
|
931
|
+
async saveState() {
|
|
932
|
+
if (!this.session) {
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
const state = {
|
|
936
|
+
session_id: this.session.session_id,
|
|
937
|
+
status: this.session.status,
|
|
938
|
+
current_phase: this.lifecycleController.getCurrentPhase(),
|
|
939
|
+
target_project: this.session.target_project,
|
|
940
|
+
started_at: this.session.started_at,
|
|
941
|
+
task_results: this.taskResults,
|
|
942
|
+
overall_status: this.overallStatus,
|
|
943
|
+
resource_stats: this.resourceStats,
|
|
944
|
+
saved_at: new Date().toISOString(),
|
|
945
|
+
};
|
|
946
|
+
const statePath = path.join(this.sessionDir, 'session.json');
|
|
947
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Complete the session - update status to COMPLETED or FAILED
|
|
951
|
+
* Must be called before saveState() to ensure correct status is persisted
|
|
952
|
+
*/
|
|
953
|
+
async completeSession(failed = false) {
|
|
954
|
+
if (!this.session) {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
// Update session status
|
|
958
|
+
this.session.status = failed ? session_1.SessionStatus.FAILED : session_1.SessionStatus.COMPLETED;
|
|
959
|
+
// Complete the active run in TaskLogManager if exists
|
|
960
|
+
if (this.taskLogManager && this.session.session_id && this.taskLogRun) {
|
|
961
|
+
try {
|
|
962
|
+
const runStatus = failed ? 'FAILED' : 'COMPLETED';
|
|
963
|
+
await this.taskLogManager.completeRun(this.session.session_id, this.taskLogRun.run_id, runStatus);
|
|
964
|
+
}
|
|
965
|
+
catch {
|
|
966
|
+
// Ignore errors completing run - best effort
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Resume from a saved session
|
|
972
|
+
*/
|
|
973
|
+
async resume(sessionId) {
|
|
974
|
+
const sessionDir = path.join(this.options.evidenceDir, sessionId);
|
|
975
|
+
const statePath = path.join(sessionDir, 'session.json');
|
|
976
|
+
if (!fs.existsSync(statePath)) {
|
|
977
|
+
throw new RunnerCoreError(error_codes_1.ErrorCode.E205_SESSION_RESUME_FAILURE, `Session state not found: ${sessionId}`, { sessionId });
|
|
978
|
+
}
|
|
979
|
+
const stateContent = fs.readFileSync(statePath, 'utf-8');
|
|
980
|
+
const state = JSON.parse(stateContent);
|
|
981
|
+
// Check if session is completed
|
|
982
|
+
if (state.overall_status === enums_1.OverallStatus.COMPLETE ||
|
|
983
|
+
state.status === session_1.SessionStatus.COMPLETED) {
|
|
984
|
+
throw new RunnerCoreError(error_codes_1.ErrorCode.E205_SESSION_RESUME_FAILURE, 'Cannot resume completed session', { sessionId, status: state.status });
|
|
985
|
+
}
|
|
986
|
+
// Restore state
|
|
987
|
+
this.session = {
|
|
988
|
+
session_id: state.session_id,
|
|
989
|
+
started_at: state.started_at,
|
|
990
|
+
target_project: state.target_project,
|
|
991
|
+
current_phase: state.current_phase,
|
|
992
|
+
status: session_1.SessionStatus.RUNNING,
|
|
993
|
+
runner_version: state.runner_version || '0.1.0',
|
|
994
|
+
configuration: state.configuration || {},
|
|
995
|
+
continuation_approved: state.continuation_approved || false,
|
|
996
|
+
limit_violations: state.limit_violations || [],
|
|
997
|
+
};
|
|
998
|
+
this.sessionDir = sessionDir;
|
|
999
|
+
this.taskResults = state.task_results || [];
|
|
1000
|
+
this.overallStatus = state.overall_status || enums_1.OverallStatus.INCOMPLETE;
|
|
1001
|
+
this.resourceStats = state.resource_stats || this.resourceStats;
|
|
1002
|
+
// Initialize lifecycle to the saved phase
|
|
1003
|
+
this.lifecycleController.initialize(sessionId);
|
|
1004
|
+
while (this.lifecycleController.getCurrentPhase() !== state.current_phase) {
|
|
1005
|
+
const currentPhase = this.lifecycleController.getCurrentPhase();
|
|
1006
|
+
this.lifecycleController.completeCurrentPhase({
|
|
1007
|
+
evidence: this.getPhaseEvidence(currentPhase),
|
|
1008
|
+
status: enums_1.PhaseStatus.COMPLETED,
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
this.initialized = true;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Shutdown the runner
|
|
1015
|
+
*/
|
|
1016
|
+
async shutdown() {
|
|
1017
|
+
// Save state before shutdown
|
|
1018
|
+
await this.saveState();
|
|
1019
|
+
// Release all L2 executors
|
|
1020
|
+
this.l2Pool.releaseAll();
|
|
1021
|
+
// Release all L1 subagents
|
|
1022
|
+
this.l1Pool.releaseAll();
|
|
1023
|
+
// Release all locks
|
|
1024
|
+
const activeLocks = this.lockManager.getActiveLocks();
|
|
1025
|
+
for (const lock of activeLocks) {
|
|
1026
|
+
this.lockManager.releaseLock(lock.lock_id);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Generate output result
|
|
1031
|
+
*/
|
|
1032
|
+
generateOutput() {
|
|
1033
|
+
return {
|
|
1034
|
+
session_id: this.session?.session_id || '',
|
|
1035
|
+
overall_status: this.overallStatus,
|
|
1036
|
+
// next_action: true if NOT ERROR/INVALID (user can continue)
|
|
1037
|
+
next_action: this.overallStatus !== enums_1.OverallStatus.ERROR && this.overallStatus !== enums_1.OverallStatus.INVALID,
|
|
1038
|
+
incomplete_task_reasons: this.incompleteReasons.length > 0 ? this.incompleteReasons : undefined,
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Record a file operation
|
|
1043
|
+
*/
|
|
1044
|
+
recordFileOperation(filePath) {
|
|
1045
|
+
if (this.resourceStats.files_used < this.resourceLimits.max_files) {
|
|
1046
|
+
this.resourceStats.files_used++;
|
|
1047
|
+
this.resourceLimitManager.checkAndRecordFileOperation(filePath);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Check and record a file operation
|
|
1052
|
+
*/
|
|
1053
|
+
checkAndRecordFileOperation(filePath) {
|
|
1054
|
+
if (this.resourceStats.files_used >= this.resourceLimits.max_files) {
|
|
1055
|
+
return {
|
|
1056
|
+
allowed: false,
|
|
1057
|
+
violation: {
|
|
1058
|
+
limit_type: 'max_files',
|
|
1059
|
+
current: this.resourceStats.files_used,
|
|
1060
|
+
limit: this.resourceLimits.max_files,
|
|
1061
|
+
},
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
this.resourceStats.files_used++;
|
|
1065
|
+
return { allowed: true };
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Set elapsed time for testing
|
|
1069
|
+
*/
|
|
1070
|
+
setElapsedTimeForTesting(seconds) {
|
|
1071
|
+
this.elapsedTimeOverride = seconds;
|
|
1072
|
+
this.resourceLimitManager.setElapsedForTesting(seconds * 1000);
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Check time limit
|
|
1076
|
+
*/
|
|
1077
|
+
checkTimeLimit() {
|
|
1078
|
+
const elapsed = this.elapsedTimeOverride ?? this.resourceStats.elapsed_seconds;
|
|
1079
|
+
return {
|
|
1080
|
+
exceeded: elapsed > this.resourceLimits.max_seconds,
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Acquire an executor
|
|
1085
|
+
*/
|
|
1086
|
+
async acquireExecutor(executorId) {
|
|
1087
|
+
// Check L2 pool capacity
|
|
1088
|
+
if (this.l2Pool.getActiveCount() >= this.l2Pool.getMaxCapacity()) {
|
|
1089
|
+
throw new RunnerCoreError(error_codes_1.ErrorCode.E404_EXECUTOR_LIMIT_EXCEEDED, 'Executor limit exceeded', { executorId, maxCapacity: this.l2Pool.getMaxCapacity() });
|
|
1090
|
+
}
|
|
1091
|
+
this.l2Pool.acquire(executorId);
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Mark status as incomplete
|
|
1095
|
+
*/
|
|
1096
|
+
markIncomplete(reason) {
|
|
1097
|
+
if (this.overallStatus === enums_1.OverallStatus.COMPLETE) {
|
|
1098
|
+
this.overallStatus = enums_1.OverallStatus.INCOMPLETE;
|
|
1099
|
+
}
|
|
1100
|
+
this.incompleteReasons.push({ task_id: 'session', reason });
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Mark status as no evidence
|
|
1104
|
+
*/
|
|
1105
|
+
markNoEvidence(reason) {
|
|
1106
|
+
if (this.overallStatus === enums_1.OverallStatus.COMPLETE ||
|
|
1107
|
+
this.overallStatus === enums_1.OverallStatus.INCOMPLETE) {
|
|
1108
|
+
this.overallStatus = enums_1.OverallStatus.NO_EVIDENCE;
|
|
1109
|
+
}
|
|
1110
|
+
this.incompleteReasons.push({ task_id: 'evidence', reason });
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Mark status as invalid
|
|
1114
|
+
*/
|
|
1115
|
+
markInvalid(reason) {
|
|
1116
|
+
this.overallStatus = enums_1.OverallStatus.INVALID;
|
|
1117
|
+
this.incompleteReasons.push({ task_id: 'validation', reason });
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Trigger a critical error
|
|
1121
|
+
*/
|
|
1122
|
+
triggerCriticalError(error) {
|
|
1123
|
+
if (this.overallStatus !== enums_1.OverallStatus.INVALID) {
|
|
1124
|
+
this.overallStatus = enums_1.OverallStatus.ERROR;
|
|
1125
|
+
}
|
|
1126
|
+
this.errorEvidence.push({
|
|
1127
|
+
error,
|
|
1128
|
+
timestamp: new Date().toISOString(),
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
// Getter methods
|
|
1132
|
+
/**
|
|
1133
|
+
* Get session directory
|
|
1134
|
+
*/
|
|
1135
|
+
getSessionDirectory() {
|
|
1136
|
+
return this.sessionDir;
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Get current phase
|
|
1140
|
+
*/
|
|
1141
|
+
getCurrentPhase() {
|
|
1142
|
+
return this.lifecycleController.getCurrentPhase();
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Get L1 pool statistics
|
|
1146
|
+
*/
|
|
1147
|
+
getL1PoolStats() {
|
|
1148
|
+
const stats = this.l1Pool.getStatistics();
|
|
1149
|
+
return {
|
|
1150
|
+
total_capacity: stats.total_capacity,
|
|
1151
|
+
active_count: stats.active_count,
|
|
1152
|
+
available_slots: stats.available_slots,
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Get L2 pool statistics
|
|
1157
|
+
*/
|
|
1158
|
+
getL2PoolStats() {
|
|
1159
|
+
const stats = this.l2Pool.getStatistics();
|
|
1160
|
+
return {
|
|
1161
|
+
total_capacity: stats.total_capacity,
|
|
1162
|
+
active_count: stats.active_count,
|
|
1163
|
+
available_slots: stats.available_slots,
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Get session ID
|
|
1168
|
+
*/
|
|
1169
|
+
getSessionId() {
|
|
1170
|
+
return this.session?.session_id || '';
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Get task results
|
|
1174
|
+
*/
|
|
1175
|
+
getTaskResults() {
|
|
1176
|
+
return [...this.taskResults];
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Get overall status
|
|
1180
|
+
*/
|
|
1181
|
+
getOverallStatus() {
|
|
1182
|
+
return this.overallStatus;
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Get resource limits
|
|
1186
|
+
*/
|
|
1187
|
+
getResourceLimits() {
|
|
1188
|
+
return { ...this.resourceLimits };
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Get resource statistics
|
|
1192
|
+
*/
|
|
1193
|
+
getResourceStats() {
|
|
1194
|
+
return { ...this.resourceStats };
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Get session state
|
|
1198
|
+
*/
|
|
1199
|
+
getSessionState() {
|
|
1200
|
+
// Convert session status to SessionStatus if needed
|
|
1201
|
+
let status = session_1.SessionStatus.INITIALIZED;
|
|
1202
|
+
if (this.session?.status) {
|
|
1203
|
+
// If it's already a SessionStatus, use it directly
|
|
1204
|
+
if (Object.values(session_1.SessionStatus).includes(this.session.status)) {
|
|
1205
|
+
status = this.session.status;
|
|
1206
|
+
}
|
|
1207
|
+
else {
|
|
1208
|
+
// Map OverallStatus to SessionStatus
|
|
1209
|
+
switch (this.session.status) {
|
|
1210
|
+
case enums_1.OverallStatus.COMPLETE:
|
|
1211
|
+
status = session_1.SessionStatus.COMPLETED;
|
|
1212
|
+
break;
|
|
1213
|
+
case enums_1.OverallStatus.ERROR:
|
|
1214
|
+
case enums_1.OverallStatus.INVALID:
|
|
1215
|
+
case enums_1.OverallStatus.NO_EVIDENCE:
|
|
1216
|
+
status = session_1.SessionStatus.FAILED;
|
|
1217
|
+
break;
|
|
1218
|
+
case enums_1.OverallStatus.INCOMPLETE:
|
|
1219
|
+
default:
|
|
1220
|
+
status = session_1.SessionStatus.RUNNING;
|
|
1221
|
+
break;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return {
|
|
1226
|
+
session_id: this.session?.session_id || '',
|
|
1227
|
+
status,
|
|
1228
|
+
current_phase: this.lifecycleController.getCurrentPhase(),
|
|
1229
|
+
started_at: this.session?.started_at || new Date().toISOString(),
|
|
1230
|
+
target_project: this.session?.target_project || '',
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Get error evidence
|
|
1235
|
+
*/
|
|
1236
|
+
getErrorEvidence() {
|
|
1237
|
+
return [...this.errorEvidence];
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Get evidence files
|
|
1241
|
+
*/
|
|
1242
|
+
getEvidenceFiles() {
|
|
1243
|
+
if (!this.sessionDir || !fs.existsSync(this.sessionDir)) {
|
|
1244
|
+
return [];
|
|
1245
|
+
}
|
|
1246
|
+
return fs.readdirSync(this.sessionDir);
|
|
1247
|
+
}
|
|
1248
|
+
// Component getters
|
|
1249
|
+
/**
|
|
1250
|
+
* Get configuration manager
|
|
1251
|
+
*/
|
|
1252
|
+
getConfigManager() {
|
|
1253
|
+
return this.configManager;
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Get session manager
|
|
1257
|
+
*/
|
|
1258
|
+
getSessionManager() {
|
|
1259
|
+
return this.sessionManager;
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Get evidence manager
|
|
1263
|
+
*/
|
|
1264
|
+
getEvidenceManager() {
|
|
1265
|
+
return this.evidenceManager;
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Get lock manager
|
|
1269
|
+
*/
|
|
1270
|
+
getLockManager() {
|
|
1271
|
+
return this.lockManager;
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Get resource limit manager
|
|
1275
|
+
*/
|
|
1276
|
+
getResourceLimitManager() {
|
|
1277
|
+
return this.resourceLimitManager;
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Get continuation manager
|
|
1281
|
+
*/
|
|
1282
|
+
getContinuationManager() {
|
|
1283
|
+
return this.continuationManager;
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Get output manager
|
|
1287
|
+
*/
|
|
1288
|
+
getOutputManager() {
|
|
1289
|
+
return this.outputManager;
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Get lifecycle controller
|
|
1293
|
+
*/
|
|
1294
|
+
getLifecycleController() {
|
|
1295
|
+
return this.lifecycleController;
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Get L1 pool
|
|
1299
|
+
*/
|
|
1300
|
+
getL1Pool() {
|
|
1301
|
+
return this.l1Pool;
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Get L2 pool
|
|
1305
|
+
*/
|
|
1306
|
+
getL2Pool() {
|
|
1307
|
+
return this.l2Pool;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
exports.RunnerCore = RunnerCore;
|
|
1311
|
+
//# sourceMappingURL=runner-core.js.map
|