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.
Files changed (215) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +108 -0
  3. package/dist/cli/cli-interface.d.ts +150 -0
  4. package/dist/cli/cli-interface.d.ts.map +1 -0
  5. package/dist/cli/cli-interface.js +606 -0
  6. package/dist/cli/cli-interface.js.map +1 -0
  7. package/dist/cli/index.d.ts +13 -0
  8. package/dist/cli/index.d.ts.map +1 -0
  9. package/dist/cli/index.js +243 -0
  10. package/dist/cli/index.js.map +1 -0
  11. package/dist/cli/llm-sentinel.d.ts +15 -0
  12. package/dist/cli/llm-sentinel.d.ts.map +1 -0
  13. package/dist/cli/llm-sentinel.js +184 -0
  14. package/dist/cli/llm-sentinel.js.map +1 -0
  15. package/dist/config/configuration-manager.d.ts +149 -0
  16. package/dist/config/configuration-manager.d.ts.map +1 -0
  17. package/dist/config/configuration-manager.js +241 -0
  18. package/dist/config/configuration-manager.js.map +1 -0
  19. package/dist/continuation/continuation-control-manager.d.ts +154 -0
  20. package/dist/continuation/continuation-control-manager.d.ts.map +1 -0
  21. package/dist/continuation/continuation-control-manager.js +303 -0
  22. package/dist/continuation/continuation-control-manager.js.map +1 -0
  23. package/dist/core/runner-core.d.ts +474 -0
  24. package/dist/core/runner-core.d.ts.map +1 -0
  25. package/dist/core/runner-core.js +1311 -0
  26. package/dist/core/runner-core.js.map +1 -0
  27. package/dist/errors/error-codes.d.ts +105 -0
  28. package/dist/errors/error-codes.d.ts.map +1 -0
  29. package/dist/errors/error-codes.js +198 -0
  30. package/dist/errors/error-codes.js.map +1 -0
  31. package/dist/errors/runner-error.d.ts +14 -0
  32. package/dist/errors/runner-error.d.ts.map +1 -0
  33. package/dist/errors/runner-error.js +33 -0
  34. package/dist/errors/runner-error.js.map +1 -0
  35. package/dist/evidence/evidence-manager.d.ts +112 -0
  36. package/dist/evidence/evidence-manager.d.ts.map +1 -0
  37. package/dist/evidence/evidence-manager.js +337 -0
  38. package/dist/evidence/evidence-manager.js.map +1 -0
  39. package/dist/executor/claude-code-executor.d.ts +136 -0
  40. package/dist/executor/claude-code-executor.d.ts.map +1 -0
  41. package/dist/executor/claude-code-executor.js +643 -0
  42. package/dist/executor/claude-code-executor.js.map +1 -0
  43. package/dist/executor/deterministic-executor.d.ts +40 -0
  44. package/dist/executor/deterministic-executor.d.ts.map +1 -0
  45. package/dist/executor/deterministic-executor.js +269 -0
  46. package/dist/executor/deterministic-executor.js.map +1 -0
  47. package/dist/lifecycle/lifecycle-controller.d.ts +270 -0
  48. package/dist/lifecycle/lifecycle-controller.d.ts.map +1 -0
  49. package/dist/lifecycle/lifecycle-controller.js +596 -0
  50. package/dist/lifecycle/lifecycle-controller.js.map +1 -0
  51. package/dist/limits/resource-limit-manager.d.ts +200 -0
  52. package/dist/limits/resource-limit-manager.d.ts.map +1 -0
  53. package/dist/limits/resource-limit-manager.js +376 -0
  54. package/dist/limits/resource-limit-manager.js.map +1 -0
  55. package/dist/locks/lock-manager.d.ts +116 -0
  56. package/dist/locks/lock-manager.d.ts.map +1 -0
  57. package/dist/locks/lock-manager.js +306 -0
  58. package/dist/locks/lock-manager.js.map +1 -0
  59. package/dist/logging/index.d.ts +8 -0
  60. package/dist/logging/index.d.ts.map +1 -0
  61. package/dist/logging/index.js +22 -0
  62. package/dist/logging/index.js.map +1 -0
  63. package/dist/logging/sensitive-data-masker.d.ts +90 -0
  64. package/dist/logging/sensitive-data-masker.d.ts.map +1 -0
  65. package/dist/logging/sensitive-data-masker.js +228 -0
  66. package/dist/logging/sensitive-data-masker.js.map +1 -0
  67. package/dist/logging/task-log-manager.d.ts +215 -0
  68. package/dist/logging/task-log-manager.d.ts.map +1 -0
  69. package/dist/logging/task-log-manager.js +743 -0
  70. package/dist/logging/task-log-manager.js.map +1 -0
  71. package/dist/mediation/fail-closed-runner.d.ts +131 -0
  72. package/dist/mediation/fail-closed-runner.d.ts.map +1 -0
  73. package/dist/mediation/fail-closed-runner.js +245 -0
  74. package/dist/mediation/fail-closed-runner.js.map +1 -0
  75. package/dist/mediation/llm-client-with-evidence.d.ts +123 -0
  76. package/dist/mediation/llm-client-with-evidence.d.ts.map +1 -0
  77. package/dist/mediation/llm-client-with-evidence.js +245 -0
  78. package/dist/mediation/llm-client-with-evidence.js.map +1 -0
  79. package/dist/mediation/llm-client.d.ts +102 -0
  80. package/dist/mediation/llm-client.d.ts.map +1 -0
  81. package/dist/mediation/llm-client.js +206 -0
  82. package/dist/mediation/llm-client.js.map +1 -0
  83. package/dist/mediation/llm-evidence-manager.d.ts +108 -0
  84. package/dist/mediation/llm-evidence-manager.d.ts.map +1 -0
  85. package/dist/mediation/llm-evidence-manager.js +230 -0
  86. package/dist/mediation/llm-evidence-manager.js.map +1 -0
  87. package/dist/mediation/llm-mediation-layer.d.ts +175 -0
  88. package/dist/mediation/llm-mediation-layer.d.ts.map +1 -0
  89. package/dist/mediation/llm-mediation-layer.js +315 -0
  90. package/dist/mediation/llm-mediation-layer.js.map +1 -0
  91. package/dist/mediation/llm-sentinel.d.ts +107 -0
  92. package/dist/mediation/llm-sentinel.d.ts.map +1 -0
  93. package/dist/mediation/llm-sentinel.js +187 -0
  94. package/dist/mediation/llm-sentinel.js.map +1 -0
  95. package/dist/mediation/real-llm-mediation-layer.d.ts +104 -0
  96. package/dist/mediation/real-llm-mediation-layer.d.ts.map +1 -0
  97. package/dist/mediation/real-llm-mediation-layer.js +322 -0
  98. package/dist/mediation/real-llm-mediation-layer.js.map +1 -0
  99. package/dist/mediation/verdict-reporter.d.ts +61 -0
  100. package/dist/mediation/verdict-reporter.d.ts.map +1 -0
  101. package/dist/mediation/verdict-reporter.js +178 -0
  102. package/dist/mediation/verdict-reporter.js.map +1 -0
  103. package/dist/models/enums.d.ts +133 -0
  104. package/dist/models/enums.d.ts.map +1 -0
  105. package/dist/models/enums.js +201 -0
  106. package/dist/models/enums.js.map +1 -0
  107. package/dist/models/evidence.d.ts +60 -0
  108. package/dist/models/evidence.d.ts.map +1 -0
  109. package/dist/models/evidence.js +135 -0
  110. package/dist/models/evidence.js.map +1 -0
  111. package/dist/models/execution-result.d.ts +89 -0
  112. package/dist/models/execution-result.d.ts.map +1 -0
  113. package/dist/models/execution-result.js +197 -0
  114. package/dist/models/execution-result.js.map +1 -0
  115. package/dist/models/file-lock.d.ts +62 -0
  116. package/dist/models/file-lock.d.ts.map +1 -0
  117. package/dist/models/file-lock.js +133 -0
  118. package/dist/models/file-lock.js.map +1 -0
  119. package/dist/models/index.d.ts +12 -0
  120. package/dist/models/index.d.ts.map +1 -0
  121. package/dist/models/index.js +91 -0
  122. package/dist/models/index.js.map +1 -0
  123. package/dist/models/repl/index.d.ts +7 -0
  124. package/dist/models/repl/index.d.ts.map +1 -0
  125. package/dist/models/repl/index.js +32 -0
  126. package/dist/models/repl/index.js.map +1 -0
  127. package/dist/models/repl/model-registry.d.ts +73 -0
  128. package/dist/models/repl/model-registry.d.ts.map +1 -0
  129. package/dist/models/repl/model-registry.js +116 -0
  130. package/dist/models/repl/model-registry.js.map +1 -0
  131. package/dist/models/repl/repl-state.d.ts +86 -0
  132. package/dist/models/repl/repl-state.d.ts.map +1 -0
  133. package/dist/models/repl/repl-state.js +152 -0
  134. package/dist/models/repl/repl-state.js.map +1 -0
  135. package/dist/models/repl/task-log.d.ts +247 -0
  136. package/dist/models/repl/task-log.d.ts.map +1 -0
  137. package/dist/models/repl/task-log.js +178 -0
  138. package/dist/models/repl/task-log.js.map +1 -0
  139. package/dist/models/session.d.ts +71 -0
  140. package/dist/models/session.d.ts.map +1 -0
  141. package/dist/models/session.js +140 -0
  142. package/dist/models/session.js.map +1 -0
  143. package/dist/models/supporting.d.ts +97 -0
  144. package/dist/models/supporting.d.ts.map +1 -0
  145. package/dist/models/supporting.js +208 -0
  146. package/dist/models/supporting.js.map +1 -0
  147. package/dist/models/task.d.ts +77 -0
  148. package/dist/models/task.d.ts.map +1 -0
  149. package/dist/models/task.js +170 -0
  150. package/dist/models/task.js.map +1 -0
  151. package/dist/output/output-control-manager.d.ts +217 -0
  152. package/dist/output/output-control-manager.d.ts.map +1 -0
  153. package/dist/output/output-control-manager.js +378 -0
  154. package/dist/output/output-control-manager.js.map +1 -0
  155. package/dist/pool/agent-pool.d.ts +284 -0
  156. package/dist/pool/agent-pool.d.ts.map +1 -0
  157. package/dist/pool/agent-pool.js +451 -0
  158. package/dist/pool/agent-pool.js.map +1 -0
  159. package/dist/repl/commands/index.d.ts +12 -0
  160. package/dist/repl/commands/index.d.ts.map +1 -0
  161. package/dist/repl/commands/index.js +26 -0
  162. package/dist/repl/commands/index.js.map +1 -0
  163. package/dist/repl/commands/init.d.ts +31 -0
  164. package/dist/repl/commands/init.d.ts.map +1 -0
  165. package/dist/repl/commands/init.js +234 -0
  166. package/dist/repl/commands/init.js.map +1 -0
  167. package/dist/repl/commands/keys.d.ts +63 -0
  168. package/dist/repl/commands/keys.d.ts.map +1 -0
  169. package/dist/repl/commands/keys.js +114 -0
  170. package/dist/repl/commands/keys.js.map +1 -0
  171. package/dist/repl/commands/logs.d.ts +91 -0
  172. package/dist/repl/commands/logs.d.ts.map +1 -0
  173. package/dist/repl/commands/logs.js +200 -0
  174. package/dist/repl/commands/logs.js.map +1 -0
  175. package/dist/repl/commands/model.d.ts +85 -0
  176. package/dist/repl/commands/model.d.ts.map +1 -0
  177. package/dist/repl/commands/model.js +225 -0
  178. package/dist/repl/commands/model.js.map +1 -0
  179. package/dist/repl/commands/models.d.ts +50 -0
  180. package/dist/repl/commands/models.d.ts.map +1 -0
  181. package/dist/repl/commands/models.js +180 -0
  182. package/dist/repl/commands/models.js.map +1 -0
  183. package/dist/repl/commands/provider.d.ts +79 -0
  184. package/dist/repl/commands/provider.d.ts.map +1 -0
  185. package/dist/repl/commands/provider.js +291 -0
  186. package/dist/repl/commands/provider.js.map +1 -0
  187. package/dist/repl/commands/session.d.ts +50 -0
  188. package/dist/repl/commands/session.d.ts.map +1 -0
  189. package/dist/repl/commands/session.js +152 -0
  190. package/dist/repl/commands/session.js.map +1 -0
  191. package/dist/repl/commands/status.d.ts +55 -0
  192. package/dist/repl/commands/status.d.ts.map +1 -0
  193. package/dist/repl/commands/status.js +182 -0
  194. package/dist/repl/commands/status.js.map +1 -0
  195. package/dist/repl/index.d.ts +6 -0
  196. package/dist/repl/index.d.ts.map +1 -0
  197. package/dist/repl/index.js +25 -0
  198. package/dist/repl/index.js.map +1 -0
  199. package/dist/repl/repl-interface.d.ts +371 -0
  200. package/dist/repl/repl-interface.d.ts.map +1 -0
  201. package/dist/repl/repl-interface.js +1214 -0
  202. package/dist/repl/repl-interface.js.map +1 -0
  203. package/dist/session/session-manager.d.ts +85 -0
  204. package/dist/session/session-manager.d.ts.map +1 -0
  205. package/dist/session/session-manager.js +217 -0
  206. package/dist/session/session-manager.js.map +1 -0
  207. package/dist/supervisor/executor-supervisor.d.ts +90 -0
  208. package/dist/supervisor/executor-supervisor.d.ts.map +1 -0
  209. package/dist/supervisor/executor-supervisor.js +223 -0
  210. package/dist/supervisor/executor-supervisor.js.map +1 -0
  211. package/dist/supervisor/index.d.ts +5 -0
  212. package/dist/supervisor/index.d.ts.map +1 -0
  213. package/dist/supervisor/index.js +9 -0
  214. package/dist/supervisor/index.js.map +1 -0
  215. package/package.json +57 -0
@@ -0,0 +1,1214 @@
1
+ "use strict";
2
+ /**
3
+ * REPL Interface for PM Orchestrator Runner
4
+ * Provides an interactive CLI experience with slash commands
5
+ *
6
+ * Supports two execution modes (per spec 10_REPL_UX.md):
7
+ * - Interactive mode: TTY connected, readline with prompt
8
+ * - Non-interactive mode: stdin script / heredoc / pipe
9
+ *
10
+ * Non-interactive mode guarantees:
11
+ * - Sequential Processing: Each command completes before next starts
12
+ * - Output Flush: All stdout is flushed before exit
13
+ * - Deterministic Exit Code: 0=COMPLETE, 1=ERROR, 2=INCOMPLETE
14
+ *
15
+ * Project Mode (per spec 10_REPL_UX.md, Property 32, 33):
16
+ * - temp: Use temporary directory (default, cleaned up on exit)
17
+ * - fixed: Use specified directory (persists after exit)
18
+ */
19
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ var desc = Object.getOwnPropertyDescriptor(m, k);
22
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
23
+ desc = { enumerable: true, get: function() { return m[k]; } };
24
+ }
25
+ Object.defineProperty(o, k2, desc);
26
+ }) : (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ o[k2] = m[k];
29
+ }));
30
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
31
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
32
+ }) : function(o, v) {
33
+ o["default"] = v;
34
+ });
35
+ var __importStar = (this && this.__importStar) || (function () {
36
+ var ownKeys = function(o) {
37
+ ownKeys = Object.getOwnPropertyNames || function (o) {
38
+ var ar = [];
39
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
40
+ return ar;
41
+ };
42
+ return ownKeys(o);
43
+ };
44
+ return function (mod) {
45
+ if (mod && mod.__esModule) return mod;
46
+ var result = {};
47
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
48
+ __setModuleDefault(result, mod);
49
+ return result;
50
+ };
51
+ })();
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.REPLInterface = exports.EXIT_CODES = void 0;
54
+ const readline = __importStar(require("readline"));
55
+ const path = __importStar(require("path"));
56
+ const fs = __importStar(require("fs"));
57
+ const os = __importStar(require("os"));
58
+ const events_1 = require("events");
59
+ const enums_1 = require("../models/enums");
60
+ const executor_supervisor_1 = require("../supervisor/executor-supervisor");
61
+ const init_1 = require("./commands/init");
62
+ const model_1 = require("./commands/model");
63
+ const session_1 = require("./commands/session");
64
+ const status_1 = require("./commands/status");
65
+ const provider_1 = require("./commands/provider");
66
+ const models_1 = require("./commands/models");
67
+ const keys_1 = require("./commands/keys");
68
+ const logs_1 = require("./commands/logs");
69
+ /**
70
+ * Exit codes for non-interactive mode - per spec 10_REPL_UX.md
71
+ * These are deterministic based on session state
72
+ */
73
+ exports.EXIT_CODES = {
74
+ COMPLETE: 0, // All tasks completed successfully
75
+ ERROR: 1, // Error occurred during execution
76
+ INCOMPLETE: 2, // Session ended with incomplete tasks
77
+ };
78
+ /**
79
+ * Commands allowed in init-only mode
80
+ * When .claude is missing, only these commands are available
81
+ */
82
+ const INIT_ONLY_ALLOWED_COMMANDS = ['help', 'init', 'exit'];
83
+ /**
84
+ * All known commands (for unknown command detection)
85
+ * Updated per spec 10_REPL_UX.md to include new commands
86
+ */
87
+ const KNOWN_COMMANDS = [
88
+ 'help', 'init', 'model', 'start', 'continue',
89
+ 'status', 'tasks', 'approve', 'exit',
90
+ // New commands per spec 10_REPL_UX.md
91
+ 'provider', 'models', 'keys', 'logs'
92
+ ];
93
+ /**
94
+ * Commands with slash prefix for tab completion
95
+ * Per spec 10_REPL_UX.md: Tab completion for slash commands
96
+ */
97
+ const SLASH_COMMANDS = KNOWN_COMMANDS.map(cmd => '/' + cmd);
98
+ /**
99
+ * REPL Interface class
100
+ */
101
+ class REPLInterface extends events_1.EventEmitter {
102
+ config;
103
+ rl = null;
104
+ session;
105
+ running = false;
106
+ initOnlyMode = false;
107
+ // Sequential input processing (prevents race conditions with piped input)
108
+ inputQueue = [];
109
+ isProcessingInput = false;
110
+ // Non-interactive mode support (per spec 10_REPL_UX.md)
111
+ executionMode;
112
+ exitCode = exports.EXIT_CODES.COMPLETE;
113
+ hasError = false;
114
+ hasIncompleteTasks = false;
115
+ // Session completion tracking (prevents double completion)
116
+ sessionCompleted = false;
117
+ // Project mode support (per spec 10_REPL_UX.md, Property 32, 33)
118
+ projectMode;
119
+ verificationRoot;
120
+ tempVerificationRoot = null;
121
+ // Command handlers
122
+ initCommand;
123
+ modelCommand;
124
+ sessionCommands;
125
+ statusCommands;
126
+ // New command handlers per spec 10_REPL_UX.md
127
+ providerCommand;
128
+ modelsCommand;
129
+ keysCommand;
130
+ logsCommand;
131
+ constructor(config = {}) {
132
+ super();
133
+ // Validate fixed mode configuration (Property 32)
134
+ if (config.projectMode === 'fixed') {
135
+ if (!config.projectRoot) {
136
+ throw new Error('project-root is required when project-mode is fixed');
137
+ }
138
+ if (!fs.existsSync(config.projectRoot)) {
139
+ throw new Error('project-root does not exist: ' + config.projectRoot);
140
+ }
141
+ }
142
+ // Determine project mode
143
+ this.projectMode = config.projectMode || 'temp';
144
+ // Set verification root based on mode
145
+ if (this.projectMode === 'fixed') {
146
+ this.verificationRoot = config.projectRoot;
147
+ }
148
+ else {
149
+ // Temp mode: create temporary directory immediately (synchronous)
150
+ // This ensures getVerificationRoot() always returns a valid path
151
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-runner-'));
152
+ this.tempVerificationRoot = tempDir;
153
+ this.verificationRoot = tempDir;
154
+ // Create minimal .claude structure to avoid init-only mode
155
+ const claudeDir = path.join(tempDir, '.claude');
156
+ fs.mkdirSync(claudeDir, { recursive: true });
157
+ const claudeMdContent = '# Temporary Project\n\nCreated by pm-orchestrator-runner in temp mode.\n';
158
+ fs.writeFileSync(path.join(claudeDir, 'CLAUDE.md'), claudeMdContent, 'utf-8');
159
+ const settingsContent = JSON.stringify({
160
+ project: { name: 'temp-project', version: '1.0.0' },
161
+ pm: { autoStart: false },
162
+ }, null, 2);
163
+ fs.writeFileSync(path.join(claudeDir, 'settings.json'), settingsContent, 'utf-8');
164
+ }
165
+ // CRITICAL: In temp mode, use verificationRoot for projectPath and evidenceDir
166
+ // This ensures files are created in the temp directory, not process.cwd()
167
+ // In fixed mode, use projectRoot (from --project-root) if provided
168
+ const resolvedProjectPath = this.projectMode === 'temp'
169
+ ? this.verificationRoot
170
+ : (config.projectRoot || config.projectPath || process.cwd());
171
+ this.config = {
172
+ projectPath: resolvedProjectPath,
173
+ evidenceDir: config.evidenceDir || path.join(resolvedProjectPath, '.claude', 'evidence'),
174
+ prompt: config.prompt || 'pm> ',
175
+ authMode: config.authMode || 'claude-code',
176
+ timeout: config.timeout || 120000,
177
+ forceNonInteractive: config.forceNonInteractive,
178
+ projectMode: config.projectMode,
179
+ projectRoot: config.projectRoot,
180
+ printProjectPath: config.printProjectPath,
181
+ };
182
+ // Detect execution mode (per spec 10_REPL_UX.md)
183
+ // Non-interactive when: stdin is not TTY, or forced via config/env
184
+ this.executionMode = this.detectExecutionMode();
185
+ this.session = {
186
+ sessionId: null,
187
+ projectPath: this.config.projectPath,
188
+ runner: null,
189
+ supervisor: null,
190
+ status: 'idle',
191
+ };
192
+ // Initialize command handlers
193
+ this.initCommand = new init_1.InitCommand();
194
+ this.modelCommand = new model_1.ModelCommand();
195
+ this.sessionCommands = new session_1.SessionCommands(this.session, this.config);
196
+ this.statusCommands = new status_1.StatusCommands(this.session);
197
+ // Initialize new command handlers
198
+ this.providerCommand = new provider_1.ProviderCommand();
199
+ this.modelsCommand = new models_1.ModelsCommand();
200
+ this.keysCommand = new keys_1.KeysCommand();
201
+ this.logsCommand = new logs_1.LogsCommand();
202
+ }
203
+ /**
204
+ * Get project mode - per spec 10_REPL_UX.md
205
+ * @returns Current project mode ('temp' or 'fixed')
206
+ */
207
+ getProjectMode() {
208
+ return this.projectMode;
209
+ }
210
+ /**
211
+ * Get verification root - per spec Property 32, 33
212
+ * @returns Absolute path to verification root directory
213
+ */
214
+ getVerificationRoot() {
215
+ return this.verificationRoot;
216
+ }
217
+ /**
218
+ * Initialize for use - called by start() or manually for testing
219
+ * Handles project path setup and PROJECT_PATH output
220
+ */
221
+ async initialize() {
222
+ // Initialize temp project root if in temp mode
223
+ if (this.projectMode === 'temp' && !this.verificationRoot) {
224
+ await this.initializeTempProjectRoot();
225
+ }
226
+ // Output PROJECT_PATH if requested (per spec 10_REPL_UX.md)
227
+ if (this.config.printProjectPath) {
228
+ console.log('PROJECT_PATH=' + this.verificationRoot);
229
+ }
230
+ }
231
+ /**
232
+ * Initialize temporary project root - per spec Property 32
233
+ * Creates a temporary directory for verification_root in temp mode
234
+ * Also creates minimal .claude structure to avoid init-only mode
235
+ */
236
+ async initializeTempProjectRoot() {
237
+ if (this.projectMode !== 'temp') {
238
+ return;
239
+ }
240
+ // Create temp directory
241
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-runner-'));
242
+ this.tempVerificationRoot = tempDir;
243
+ this.verificationRoot = tempDir;
244
+ // Create minimal .claude structure for temp mode
245
+ // This prevents init-only mode and allows /start to work immediately
246
+ const claudeDir = path.join(tempDir, '.claude');
247
+ fs.mkdirSync(claudeDir, { recursive: true });
248
+ // Create minimal CLAUDE.md
249
+ const claudeMdContent = '# Temporary Project\n\nCreated by pm-orchestrator-runner in temp mode.\n';
250
+ fs.writeFileSync(path.join(claudeDir, 'CLAUDE.md'), claudeMdContent, 'utf-8');
251
+ // Create minimal settings.json
252
+ const settingsContent = JSON.stringify({
253
+ project: { name: 'temp-project', version: '1.0.0' },
254
+ pm: { autoStart: false },
255
+ }, null, 2);
256
+ fs.writeFileSync(path.join(claudeDir, 'settings.json'), settingsContent, 'utf-8');
257
+ // Update config.projectPath to point to temp directory
258
+ // This ensures validateProjectStructure checks the right path
259
+ this.config.projectPath = tempDir;
260
+ this.session.projectPath = tempDir;
261
+ }
262
+ /**
263
+ * Verify files and return verification records - per spec Property 33
264
+ * @param absolutePaths - Array of absolute file paths to verify
265
+ * @returns Array of VerifiedFile records with relative paths
266
+ */
267
+ verifyFiles(absolutePaths) {
268
+ const now = new Date().toISOString();
269
+ return absolutePaths.map(absolutePath => {
270
+ // Convert to relative path from verification_root
271
+ const relativePath = path.relative(this.verificationRoot, absolutePath);
272
+ return {
273
+ path: relativePath,
274
+ exists: fs.existsSync(absolutePath),
275
+ detected_at: now,
276
+ detection_method: 'diff',
277
+ };
278
+ });
279
+ }
280
+ /**
281
+ * Create a TaskLog with verification info - per spec Property 33
282
+ * @param taskId - Task identifier
283
+ * @param description - Task description
284
+ * @returns TaskLog with verification_root populated
285
+ */
286
+ createTaskLog(taskId, description) {
287
+ return {
288
+ task_id: taskId,
289
+ description,
290
+ verification_root: this.verificationRoot,
291
+ created_at: new Date().toISOString(),
292
+ };
293
+ }
294
+ /**
295
+ * Cleanup resources - handles temp directory cleanup
296
+ * Per spec Property 32: temp mode directories may be cleaned up
297
+ */
298
+ async cleanup() {
299
+ if (this.session.supervisor) {
300
+ this.session.supervisor.stop();
301
+ }
302
+ // Note: In temp mode, we don't forcefully delete the temp directory
303
+ // The OS will clean it up eventually, and leaving it allows for debugging
304
+ // In fixed mode, the directory persists as expected
305
+ }
306
+ /**
307
+ * Start the REPL
308
+ * Per spec 10_REPL_UX.md L45: validate project structure on startup
309
+ *
310
+ * If .claude is missing, enter init-only mode instead of throwing.
311
+ * In init-only mode, only /help, /init, /exit are available.
312
+ */
313
+ async start() {
314
+ // Initialize project root
315
+ await this.initialize();
316
+ // Validate project structure per spec 10_REPL_UX.md L45
317
+ const validation = await this.validateProjectStructure();
318
+ if (!validation.valid) {
319
+ // Enter init-only mode instead of failing
320
+ this.initOnlyMode = true;
321
+ console.log('');
322
+ console.log('WARNING: Project not initialized');
323
+ for (const error of validation.errors) {
324
+ console.log(' - ' + error);
325
+ }
326
+ console.log('');
327
+ console.log('Entering init-only mode.');
328
+ console.log('Only /help, /init, and /exit are available.');
329
+ console.log('Run /init to initialize the project structure.');
330
+ console.log('');
331
+ }
332
+ else {
333
+ this.initOnlyMode = false;
334
+ }
335
+ this.running = true;
336
+ this.rl = readline.createInterface({
337
+ input: process.stdin,
338
+ output: process.stdout,
339
+ prompt: this.config.prompt,
340
+ completer: this.completer.bind(this),
341
+ });
342
+ // Print welcome message (or init-only mode reminder)
343
+ if (!this.initOnlyMode) {
344
+ this.printWelcome();
345
+ }
346
+ this.rl.prompt();
347
+ this.rl.on('line', (line) => {
348
+ // Queue-based sequential processing to prevent race conditions
349
+ // This ensures /start completes before subsequent commands are processed
350
+ this.enqueueInput(line);
351
+ });
352
+ // Return a promise that resolves when REPL exits
353
+ // CRITICAL: Single 'close' handler to avoid race conditions
354
+ // Previous bug: two handlers - one waiting for queue drain, one resolving immediately
355
+ return new Promise((resolve) => {
356
+ this.rl.on('close', async () => {
357
+ // In non-interactive mode, wait for all queued input to be processed
358
+ // This ensures all commands complete before EOF handling
359
+ if (this.executionMode === 'non_interactive') {
360
+ // Wait for input queue to drain, OR exit immediately if running is false
361
+ // running = false means /exit was called, so remaining queue items should be ignored
362
+ while ((this.inputQueue.length > 0 || this.isProcessingInput) && this.running) {
363
+ await new Promise(r => setTimeout(r, 10));
364
+ }
365
+ }
366
+ this.running = false;
367
+ // Ensure all output is flushed before cleanup
368
+ await this.flushStdout();
369
+ await this.cleanup();
370
+ // In non-interactive mode, set process exit code
371
+ if (this.executionMode === 'non_interactive') {
372
+ this.updateExitCode();
373
+ process.exitCode = this.exitCode;
374
+ }
375
+ resolve();
376
+ });
377
+ });
378
+ }
379
+ /**
380
+ * Process input line
381
+ */
382
+ async processInput(input) {
383
+ if (input.startsWith('/')) {
384
+ await this.processCommand(input);
385
+ }
386
+ else {
387
+ await this.processNaturalLanguage(input);
388
+ }
389
+ }
390
+ /**
391
+ * Enqueue input for sequential processing
392
+ * This prevents race conditions when piped input arrives faster than processing
393
+ */
394
+ enqueueInput(line) {
395
+ const trimmed = line.trim();
396
+ if (!trimmed) {
397
+ // Empty line - just show prompt if not processing
398
+ if (!this.isProcessingInput && this.running) {
399
+ this.rl?.prompt();
400
+ }
401
+ return;
402
+ }
403
+ this.inputQueue.push(trimmed);
404
+ // Start processing if not already processing
405
+ this.processQueue();
406
+ }
407
+ /**
408
+ * Process queued inputs sequentially
409
+ * Ensures each input completes before the next one starts
410
+ * Per spec 10_REPL_UX.md: Sequential Processing Guarantee
411
+ */
412
+ async processQueue() {
413
+ // Already processing - let the current processor handle the queue
414
+ if (this.isProcessingInput) {
415
+ return;
416
+ }
417
+ this.isProcessingInput = true;
418
+ try {
419
+ while (this.inputQueue.length > 0 && this.running) {
420
+ const input = this.inputQueue.shift();
421
+ try {
422
+ await this.processInput(input);
423
+ }
424
+ catch (err) {
425
+ this.printError(err);
426
+ }
427
+ // In non-interactive mode, flush output after each command
428
+ // This ensures output is visible before processing next command
429
+ if (this.executionMode === 'non_interactive') {
430
+ await this.flushStdout();
431
+ }
432
+ // Show prompt after each input if still running (interactive mode only)
433
+ if (this.running && this.inputQueue.length === 0) {
434
+ if (this.executionMode === 'interactive') {
435
+ this.rl?.prompt();
436
+ }
437
+ }
438
+ }
439
+ }
440
+ finally {
441
+ this.isProcessingInput = false;
442
+ }
443
+ }
444
+ /**
445
+ * Process slash command
446
+ * Per spec 10_REPL_UX.md L66: All commands must return a status (fail-closed)
447
+ *
448
+ * In init-only mode, only /help, /init, /exit are allowed.
449
+ * Other commands return ERROR with instruction to run /init first.
450
+ */
451
+ async processCommand(input) {
452
+ const parts = input.slice(1).split(/\s+/);
453
+ const command = parts[0].toLowerCase();
454
+ const args = parts.slice(1);
455
+ // First check if command is known (E201 for unknown takes precedence)
456
+ if (!KNOWN_COMMANDS.includes(command)) {
457
+ // Per spec 10_REPL_UX.md L66: Unknown commands must return ERROR
458
+ this.print('Unknown command: /' + command);
459
+ this.print('Type /help for available commands.');
460
+ return {
461
+ success: false,
462
+ error: {
463
+ code: 'E201',
464
+ message: 'Unknown command: /' + command + '. Type /help for available commands.',
465
+ },
466
+ };
467
+ }
468
+ // Check if in init-only mode
469
+ const isInitOnly = await this.isInitOnlyMode();
470
+ // In init-only mode, block commands that require .claude
471
+ if (isInitOnly && !INIT_ONLY_ALLOWED_COMMANDS.includes(command)) {
472
+ this.print('ERROR: Cannot run /' + command + ' - project not initialized.');
473
+ this.print('Run /init first to create .claude directory.');
474
+ return {
475
+ success: false,
476
+ error: {
477
+ code: 'E301',
478
+ message: 'Cannot run /' + command + ' in init-only mode. Run /init first to initialize the project.',
479
+ },
480
+ };
481
+ }
482
+ switch (command) {
483
+ case 'help':
484
+ this.printHelp();
485
+ return { success: true };
486
+ case 'init':
487
+ return await this.handleInit(args);
488
+ case 'model':
489
+ return await this.handleModel(args);
490
+ case 'tasks':
491
+ await this.handleTasks();
492
+ return { success: true };
493
+ case 'status':
494
+ await this.handleStatus();
495
+ return { success: true };
496
+ case 'start':
497
+ return await this.handleStart(args);
498
+ case 'continue':
499
+ return await this.handleContinue(args);
500
+ case 'approve':
501
+ return await this.handleApprove();
502
+ case 'exit':
503
+ await this.handleExit();
504
+ return { success: true };
505
+ // New commands per spec 10_REPL_UX.md
506
+ case 'provider':
507
+ return await this.handleProvider(args);
508
+ case 'models':
509
+ return await this.handleModels(args);
510
+ case 'keys':
511
+ return await this.handleKeys(args);
512
+ case 'logs':
513
+ return await this.handleLogs(args);
514
+ default:
515
+ // This should never be reached since unknown commands are handled above
516
+ // If we reach here, KNOWN_COMMANDS list is inconsistent with switch cases
517
+ throw new Error('Internal error: command "' + command + '" is in KNOWN_COMMANDS but not handled in switch');
518
+ }
519
+ }
520
+ /**
521
+ * Process natural language input
522
+ * Per spec 10_REPL_UX.md L117-118: Model selection is REPL-local
523
+ * Model is read from .claude/repl.json and passed to executor via runner
524
+ */
525
+ async processNaturalLanguage(input) {
526
+ console.log(`[DEBUG processNaturalLanguage] start, input="${input}"`);
527
+ if (this.session.status !== 'running') {
528
+ this.print('No active session. Use /start to begin, or /init to set up a new project.');
529
+ return;
530
+ }
531
+ if (!this.session.runner) {
532
+ this.print('Runner not initialized. Use /start first.');
533
+ return;
534
+ }
535
+ this.print('Processing task: ' + input);
536
+ this.print('');
537
+ try {
538
+ // Per spec 10_REPL_UX.md L117-118: Get selected model from REPL config
539
+ // Model is REPL-local, stored in .claude/repl.json
540
+ let selectedModel;
541
+ const modelResult = await this.modelCommand.getModel(this.session.projectPath);
542
+ if (modelResult.success && modelResult.model && modelResult.model !== 'UNSET') {
543
+ selectedModel = modelResult.model;
544
+ }
545
+ console.log(`[DEBUG processNaturalLanguage] calling runner.execute...`);
546
+ // Create task from natural language input
547
+ // CRITICAL: naturalLanguageTask must be set to trigger Claude Code execution
548
+ // Without this, the task would only use fallback file creation patterns
549
+ const result = await this.session.runner.execute({
550
+ tasks: [{
551
+ id: 'task-' + Date.now(),
552
+ description: input,
553
+ naturalLanguageTask: input,
554
+ }],
555
+ selectedModel,
556
+ });
557
+ console.log(`[DEBUG processNaturalLanguage] runner.execute returned, status=${result.overall_status}`);
558
+ this.printExecutionResult(result);
559
+ }
560
+ catch (err) {
561
+ console.log(`[DEBUG processNaturalLanguage] error: ${err.message}`);
562
+ this.printError(err);
563
+ }
564
+ console.log(`[DEBUG processNaturalLanguage] done`);
565
+ }
566
+ /**
567
+ * Print welcome message
568
+ */
569
+ printWelcome() {
570
+ this.print('');
571
+ this.print('PM Orchestrator Runner - Interactive Mode');
572
+ this.print('=========================================');
573
+ this.print('Project: ' + this.session.projectPath);
574
+ this.print('Auth Mode: ' + this.config.authMode + ' (L1 uses Claude Code CLI)');
575
+ this.print('Project Mode: ' + this.projectMode);
576
+ if (this.projectMode === 'fixed') {
577
+ this.print('Verification Root: ' + this.verificationRoot);
578
+ }
579
+ this.print('');
580
+ this.print('Type /help for available commands, or just describe your task.');
581
+ this.print('');
582
+ }
583
+ /**
584
+ * Print help
585
+ */
586
+ printHelp() {
587
+ this.print('');
588
+ this.print('PM Orchestrator Runner - Commands');
589
+ this.print('=================================');
590
+ this.print('');
591
+ this.print(' /help Show this help message');
592
+ this.print(' /init Initialize .claude/ directory in current project');
593
+ this.print('');
594
+ this.print('Provider/Model Configuration:');
595
+ this.print(' /provider Show current provider');
596
+ this.print(' /provider show List all available providers');
597
+ this.print(' /provider <name> Set provider (claude-code, openai, anthropic)');
598
+ this.print(' /models List models for current provider');
599
+ this.print(' /models <provider> List models for specific provider');
600
+ this.print(' /model Show current model configuration');
601
+ this.print(' /model <name> Set model (saves to .claude/repl.json)');
602
+ this.print(' /keys Show API key status (SET/NOT SET)');
603
+ this.print('');
604
+ this.print('Session Management:');
605
+ this.print(' /start [path] Start a new session (optional project path)');
606
+ this.print(' /continue [session] Continue a paused session');
607
+ this.print(' /status Show session status and phase');
608
+ this.print(' /tasks Show tasks in current session');
609
+ this.print(' /approve Approve continuation for INCOMPLETE session');
610
+ this.print('');
611
+ this.print('Logging:');
612
+ this.print(' /logs List task logs for current session');
613
+ this.print(' /logs <task-id> Show task details (summary view)');
614
+ this.print(' /logs <task-id> --full Show task details with executor logs');
615
+ this.print('');
616
+ this.print('Other:');
617
+ this.print(' /exit Exit REPL (saves state)');
618
+ this.print('');
619
+ this.print('Natural Language:');
620
+ this.print(' Just type your task description without a slash prefix.');
621
+ this.print(' Example: "Create an HTTP server with /health endpoint"');
622
+ this.print('');
623
+ this.print('Auth Mode: ' + this.config.authMode);
624
+ this.print(' L1 (read-only subagents) use Claude Code CLI - no API key needed.');
625
+ this.print(' L2 (executor) also uses Claude Code CLI integration.');
626
+ this.print('');
627
+ }
628
+ /**
629
+ * Handle /init command
630
+ */
631
+ async handleInit(args) {
632
+ const targetPath = args[0] || this.session.projectPath;
633
+ const result = await this.initCommand.execute(targetPath);
634
+ if (result.success) {
635
+ this.print(result.message);
636
+ this.print('');
637
+ this.print('Next: /start to begin a session');
638
+ return { success: true };
639
+ }
640
+ else {
641
+ this.print('Error: ' + result.message);
642
+ return {
643
+ success: false,
644
+ error: {
645
+ code: 'E101',
646
+ message: result.message,
647
+ },
648
+ };
649
+ }
650
+ }
651
+ /**
652
+ * Handle /model command
653
+ * Per spec 10_REPL_UX.md L113-143:
654
+ * - /model displays current model or "UNSET"
655
+ * - /model <name> sets model and generates Evidence
656
+ * - .claude/ missing -> E101 ERROR
657
+ * - JSON parse error -> E105 ERROR
658
+ */
659
+ async handleModel(args) {
660
+ if (args.length === 0) {
661
+ const result = await this.modelCommand.getModel(this.session.projectPath);
662
+ if (result.success) {
663
+ // Per spec 10_REPL_UX.md L133: Display "UNSET" if not configured
664
+ this.print('Current model: ' + result.model);
665
+ if (result.configPath) {
666
+ this.print('Config file: ' + result.configPath);
667
+ }
668
+ return { success: true };
669
+ }
670
+ else {
671
+ // Per spec 10_REPL_UX.md L137-138: E101 or E105
672
+ this.print('Error [' + result.error?.code + ']: ' + (result.error?.message || result.message));
673
+ return {
674
+ success: false,
675
+ error: result.error || { code: 'E105', message: result.message || 'Unknown error' },
676
+ };
677
+ }
678
+ }
679
+ else {
680
+ const result = await this.modelCommand.setModel(this.session.projectPath, args[0]);
681
+ if (result.success) {
682
+ this.print('Model set to: ' + args[0]);
683
+ this.print('Saved to: ' + result.configPath);
684
+ if (result.evidencePath) {
685
+ this.print('Evidence: ' + result.evidencePath);
686
+ }
687
+ return { success: true };
688
+ }
689
+ else {
690
+ // Per spec 10_REPL_UX.md L137: E101 if .claude/ missing
691
+ const errorCode = result.error?.code || 'E102';
692
+ const errorMessage = result.error?.message || result.message || 'Failed to set model';
693
+ this.print('Error [' + errorCode + ']: ' + errorMessage);
694
+ return {
695
+ success: false,
696
+ error: { code: errorCode, message: errorMessage },
697
+ };
698
+ }
699
+ }
700
+ }
701
+ /**
702
+ * Handle /provider command
703
+ * Per spec 10_REPL_UX.md Section 2.1
704
+ */
705
+ async handleProvider(args) {
706
+ if (args.length === 0) {
707
+ // /provider - show current
708
+ const result = await this.providerCommand.getProvider(this.session.projectPath);
709
+ if (result.success) {
710
+ const output = this.providerCommand.formatCurrentProvider(result.provider || 'UNSET');
711
+ this.print(output);
712
+ return { success: true };
713
+ }
714
+ else {
715
+ this.print('Error [' + result.error?.code + ']: ' + (result.error?.message || result.message));
716
+ return { success: false, error: result.error };
717
+ }
718
+ }
719
+ else if (args[0] === 'show') {
720
+ // /provider show - list all
721
+ const result = await this.providerCommand.listProviders();
722
+ const currentResult = await this.providerCommand.getProvider(this.session.projectPath);
723
+ const currentProvider = currentResult.success ? currentResult.provider : undefined;
724
+ const output = this.providerCommand.formatProviderList(result.providers || [], currentProvider);
725
+ this.print(output);
726
+ return { success: true };
727
+ }
728
+ else {
729
+ // /provider <name> - set provider
730
+ const result = await this.providerCommand.setProvider(this.session.projectPath, args[0]);
731
+ if (result.success) {
732
+ this.print('Provider set to: ' + args[0]);
733
+ this.print('Saved to: ' + result.configPath);
734
+ this.print('Note: Model selection has been reset. Use /models to see available models.');
735
+ if (result.evidencePath) {
736
+ this.print('Evidence: ' + result.evidencePath);
737
+ }
738
+ return { success: true };
739
+ }
740
+ else {
741
+ this.print('Error [' + result.error?.code + ']: ' + (result.error?.message || result.message));
742
+ return { success: false, error: result.error };
743
+ }
744
+ }
745
+ }
746
+ /**
747
+ * Handle /models command
748
+ * Per spec 10_REPL_UX.md Section 2.2
749
+ */
750
+ async handleModels(args) {
751
+ const providerId = args.length > 0 ? args[0] : undefined;
752
+ const result = await this.modelsCommand.listModels(this.session.projectPath, providerId);
753
+ if (result.success) {
754
+ const output = this.modelsCommand.formatModelList(result.models || [], result.currentModel, result.provider);
755
+ this.print(output);
756
+ return { success: true };
757
+ }
758
+ else {
759
+ this.print('Error [' + result.error?.code + ']: ' + (result.error?.message || result.message));
760
+ return { success: false, error: result.error };
761
+ }
762
+ }
763
+ /**
764
+ * Handle /keys command
765
+ * Per spec 10_REPL_UX.md Section 2.3
766
+ */
767
+ async handleKeys(args) {
768
+ let result;
769
+ if (args.length > 0) {
770
+ result = await this.keysCommand.checkProviderKey(args[0]);
771
+ }
772
+ else {
773
+ result = await this.keysCommand.getKeyStatus();
774
+ }
775
+ if (result.success) {
776
+ const output = this.keysCommand.formatKeyStatus(result.keys || []);
777
+ this.print(output);
778
+ return { success: true };
779
+ }
780
+ else {
781
+ this.print('Error [' + result.error?.code + ']: ' + (result.error?.message || result.message));
782
+ return { success: false, error: result.error };
783
+ }
784
+ }
785
+ /**
786
+ * Handle /logs command
787
+ * Per spec 10_REPL_UX.md Section 2.4
788
+ */
789
+ async handleLogs(args) {
790
+ if (args.length === 0) {
791
+ // /logs - list all logs for current session
792
+ if (!this.session.sessionId) {
793
+ this.print('No active session. Use /start to begin a session.');
794
+ return {
795
+ success: false,
796
+ error: { code: 'E104', message: 'No active session' },
797
+ };
798
+ }
799
+ const result = await this.logsCommand.listLogs(this.session.projectPath, this.session.sessionId);
800
+ if (result.success) {
801
+ this.print(result.output || 'No logs found.');
802
+ return { success: true };
803
+ }
804
+ else {
805
+ this.print('Error [' + result.error?.code + ']: ' + (result.error?.message || result.message));
806
+ return { success: false, error: result.error };
807
+ }
808
+ }
809
+ else {
810
+ // /logs <task-id> [--full]
811
+ const taskId = args[0];
812
+ const full = args.includes('--full');
813
+ const result = await this.logsCommand.getTaskDetail(this.session.projectPath, taskId, full);
814
+ if (result.success) {
815
+ this.print(result.output || 'No log detail found.');
816
+ return { success: true };
817
+ }
818
+ else {
819
+ this.print('Error [' + result.error?.code + ']: ' + (result.error?.message || result.message));
820
+ return { success: false, error: result.error };
821
+ }
822
+ }
823
+ }
824
+ /**
825
+ * Handle /tasks command
826
+ */
827
+ async handleTasks() {
828
+ const result = await this.statusCommands.getTasks();
829
+ this.print(result);
830
+ }
831
+ /**
832
+ * Handle /status command
833
+ */
834
+ async handleStatus() {
835
+ const result = await this.statusCommands.getStatus();
836
+ this.print(result);
837
+ }
838
+ /**
839
+ * Handle /start command
840
+ * Per spec Property 32, 33: Use verification_root for file operations in temp mode
841
+ */
842
+ async handleStart(args) {
843
+ // In temp mode, use verificationRoot for file operations
844
+ // In fixed mode or with explicit args, use the provided/session path
845
+ const projectPath = args[0] ||
846
+ (this.projectMode === 'temp' ? this.verificationRoot : this.session.projectPath);
847
+ try {
848
+ const result = await this.sessionCommands.start(projectPath);
849
+ if (result.success) {
850
+ this.session.sessionId = result.sessionId;
851
+ this.session.runner = result.runner;
852
+ this.session.status = 'running';
853
+ // Keep session.projectPath as verificationRoot in temp mode
854
+ this.session.projectPath = projectPath;
855
+ // Initialize supervisor
856
+ const supervisorConfig = {
857
+ checkIntervalMs: 5000,
858
+ maxRetries: 3,
859
+ timeoutMs: this.config.evidenceDir ? 300000 : 300000,
860
+ };
861
+ this.session.supervisor = new executor_supervisor_1.ExecutorSupervisor(this.session.runner, supervisorConfig);
862
+ this.print('Session started: ' + result.sessionId);
863
+ this.print('Project: ' + projectPath);
864
+ this.print('');
865
+ this.print('You can now describe tasks in natural language.');
866
+ return { success: true };
867
+ }
868
+ else {
869
+ this.print('Error: ' + result.message);
870
+ return {
871
+ success: false,
872
+ error: { code: 'E103', message: result.message || 'Failed to start session' },
873
+ };
874
+ }
875
+ }
876
+ catch (err) {
877
+ this.printError(err);
878
+ return {
879
+ success: false,
880
+ error: { code: 'E103', message: err.message },
881
+ };
882
+ }
883
+ }
884
+ /**
885
+ * Handle /continue command
886
+ */
887
+ async handleContinue(args) {
888
+ const sessionId = args[0] || this.session.sessionId;
889
+ if (!sessionId) {
890
+ this.print('No session to continue. Provide a session ID or start a new session.');
891
+ return {
892
+ success: false,
893
+ error: { code: 'E104', message: 'No session to continue' },
894
+ };
895
+ }
896
+ try {
897
+ const result = await this.sessionCommands.continueSession(sessionId);
898
+ if (result.success) {
899
+ this.session.sessionId = sessionId;
900
+ this.session.runner = result.runner;
901
+ this.session.status = 'running';
902
+ this.print('Session resumed: ' + sessionId);
903
+ return { success: true };
904
+ }
905
+ else {
906
+ this.print('Error: ' + result.message);
907
+ return {
908
+ success: false,
909
+ error: { code: 'E104', message: result.message || 'Failed to continue session' },
910
+ };
911
+ }
912
+ }
913
+ catch (err) {
914
+ this.printError(err);
915
+ return {
916
+ success: false,
917
+ error: { code: 'E104', message: err.message },
918
+ };
919
+ }
920
+ }
921
+ /**
922
+ * Handle /approve command
923
+ */
924
+ async handleApprove() {
925
+ if (!this.session.sessionId) {
926
+ this.print('No active session to approve.');
927
+ return {
928
+ success: false,
929
+ error: { code: 'E106', message: 'No active session to approve' },
930
+ };
931
+ }
932
+ try {
933
+ const result = await this.sessionCommands.approve(this.session.sessionId);
934
+ if (result.success) {
935
+ this.print('Continuation approved.');
936
+ this.print('Use /continue to resume execution.');
937
+ return { success: true };
938
+ }
939
+ else {
940
+ this.print('Error: ' + result.message);
941
+ return {
942
+ success: false,
943
+ error: { code: 'E106', message: result.message || 'Failed to approve' },
944
+ };
945
+ }
946
+ }
947
+ catch (err) {
948
+ this.printError(err);
949
+ return {
950
+ success: false,
951
+ error: { code: 'E106', message: err.message },
952
+ };
953
+ }
954
+ }
955
+ /**
956
+ * Handle /exit command
957
+ * Per spec 10_REPL_UX.md: Ensure clean exit with flushed output
958
+ *
959
+ * Guarantees:
960
+ * - Session state is persisted before exit
961
+ * - All output is flushed before readline closes
962
+ * - Double-completion is prevented via sessionCompleted flag
963
+ */
964
+ async handleExit() {
965
+ // Prevent double completion (e.g., if /exit is called multiple times)
966
+ if (this.sessionCompleted) {
967
+ this.print('Session already completed, closing...');
968
+ this.running = false;
969
+ this.rl?.close();
970
+ return;
971
+ }
972
+ this.sessionCompleted = true;
973
+ this.print('Saving session state...');
974
+ if (this.session.runner && this.session.sessionId) {
975
+ try {
976
+ // Complete the session first (updates status from RUNNING to COMPLETED/FAILED)
977
+ const failed = this.hasError || this.hasIncompleteTasks;
978
+ await this.session.runner.completeSession(failed);
979
+ await this.session.runner.saveState();
980
+ this.print('Session saved: ' + this.session.sessionId);
981
+ }
982
+ catch (err) {
983
+ // Log error but ensure status is updated for exit code calculation
984
+ const errorMessage = err.message || String(err);
985
+ this.print('Warning: Could not save session state: ' + errorMessage);
986
+ // Mark as error so exit code reflects persistence failure
987
+ this.hasError = true;
988
+ this.updateExitCode();
989
+ }
990
+ }
991
+ if (this.session.supervisor) {
992
+ this.session.supervisor.stop();
993
+ }
994
+ this.print('Goodbye!');
995
+ // Flush output before closing (critical for non-interactive mode)
996
+ await this.flushStdout();
997
+ this.running = false;
998
+ this.rl?.close();
999
+ }
1000
+ /**
1001
+ * Print execution result
1002
+ * Per spec 10_REPL_UX.md: Error details must be visible for fail-closed debugging
1003
+ */
1004
+ printExecutionResult(result) {
1005
+ this.print('');
1006
+ this.print('--- Execution Result ---');
1007
+ this.print('Status: ' + result.overall_status);
1008
+ if (result.tasks_completed !== undefined) {
1009
+ this.print('Tasks: ' + result.tasks_completed + '/' + result.tasks_total + ' completed');
1010
+ // Track incomplete tasks for exit code (non-interactive mode)
1011
+ if (result.tasks_completed < (result.tasks_total || 0)) {
1012
+ this.hasIncompleteTasks = true;
1013
+ this.updateExitCode();
1014
+ }
1015
+ }
1016
+ // Display error details when status is ERROR (critical for debugging fail-closed behavior)
1017
+ if (result.error) {
1018
+ this.print('Error: ' + result.error.message);
1019
+ if (result.error.code) {
1020
+ this.print('Error Code: ' + result.error.code);
1021
+ }
1022
+ // Track error for exit code
1023
+ this.hasError = true;
1024
+ this.updateExitCode();
1025
+ }
1026
+ // Display incomplete task reasons (helps identify why tasks failed)
1027
+ if (result.incomplete_task_reasons && result.incomplete_task_reasons.length > 0) {
1028
+ this.print('Incomplete Tasks:');
1029
+ for (const reason of result.incomplete_task_reasons) {
1030
+ this.print(' - ' + reason.task_id + ': ' + reason.reason);
1031
+ }
1032
+ this.hasIncompleteTasks = true;
1033
+ this.updateExitCode();
1034
+ }
1035
+ // Track ERROR status
1036
+ if (result.overall_status === enums_1.OverallStatus.ERROR) {
1037
+ this.hasError = true;
1038
+ this.updateExitCode();
1039
+ }
1040
+ // Track INCOMPLETE status
1041
+ if (result.overall_status === enums_1.OverallStatus.INCOMPLETE) {
1042
+ this.hasIncompleteTasks = true;
1043
+ this.updateExitCode();
1044
+ }
1045
+ if (result.next_action !== undefined) {
1046
+ this.print('Next Action: ' + (result.next_action ? 'Yes' : 'No'));
1047
+ if (result.next_action_reason) {
1048
+ this.print('Reason: ' + result.next_action_reason);
1049
+ }
1050
+ }
1051
+ this.print('------------------------');
1052
+ this.print('');
1053
+ }
1054
+ /**
1055
+ * Print message with flush guarantee for non-interactive mode
1056
+ * Per spec 10_REPL_UX.md: Output Flush Guarantee
1057
+ */
1058
+ print(message) {
1059
+ console.log(message);
1060
+ }
1061
+ /**
1062
+ * Flush stdout - ensures all output is written before continuing
1063
+ * Critical for non-interactive mode where process may exit immediately after
1064
+ */
1065
+ async flushStdout() {
1066
+ return new Promise((resolve) => {
1067
+ // If stdout is already drained, resolve immediately
1068
+ if (process.stdout.write('')) {
1069
+ resolve();
1070
+ }
1071
+ else {
1072
+ // Wait for drain event
1073
+ process.stdout.once('drain', () => {
1074
+ resolve();
1075
+ });
1076
+ }
1077
+ });
1078
+ }
1079
+ /**
1080
+ * Print error
1081
+ */
1082
+ printError(err) {
1083
+ console.error('Error: ' + err.message);
1084
+ this.hasError = true;
1085
+ this.updateExitCode();
1086
+ }
1087
+ /**
1088
+ * Validate project structure - per spec 10_REPL_UX.md L45
1089
+ * "REPL の起動時点で validate 相当の検証を行い、必須構造がなければ即 ERROR とする。"
1090
+ *
1091
+ * @returns Validation result with valid flag and errors array
1092
+ */
1093
+ async validateProjectStructure() {
1094
+ const errors = [];
1095
+ const projectPath = this.config.projectPath;
1096
+ // Check .claude directory exists
1097
+ const claudeDir = path.join(projectPath, '.claude');
1098
+ if (!fs.existsSync(claudeDir)) {
1099
+ errors.push('.claude directory not found at ' + claudeDir);
1100
+ }
1101
+ else {
1102
+ // Check for required files/directories within .claude
1103
+ const requiredItems = [
1104
+ { path: path.join(claudeDir, 'CLAUDE.md'), type: 'file', name: 'CLAUDE.md' },
1105
+ { path: path.join(claudeDir, 'settings.json'), type: 'file', name: 'settings.json' },
1106
+ ];
1107
+ for (const item of requiredItems) {
1108
+ if (!fs.existsSync(item.path)) {
1109
+ errors.push('Required ' + item.type + ' not found: ' + item.name);
1110
+ }
1111
+ }
1112
+ }
1113
+ return {
1114
+ valid: errors.length === 0,
1115
+ errors,
1116
+ };
1117
+ }
1118
+ /**
1119
+ * Check if REPL is in init-only mode
1120
+ * Init-only mode is active when .claude directory is missing
1121
+ * In this mode, only /help, /init, /exit are allowed
1122
+ *
1123
+ * @returns true if in init-only mode (rechecks file system)
1124
+ */
1125
+ async isInitOnlyMode() {
1126
+ const validation = await this.validateProjectStructure();
1127
+ this.initOnlyMode = !validation.valid;
1128
+ return this.initOnlyMode;
1129
+ }
1130
+ /**
1131
+ * Tab completion for slash commands
1132
+ * Per spec 10_REPL_UX.md: Tab completion support
1133
+ *
1134
+ * @param line - Current input line
1135
+ * @returns Tuple of [completions, original line]
1136
+ */
1137
+ completer(line) {
1138
+ // Only complete if line starts with /
1139
+ if (!line.startsWith('/')) {
1140
+ return [[], line];
1141
+ }
1142
+ // Find matching commands (prefix match)
1143
+ const hits = SLASH_COMMANDS.filter(cmd => cmd.startsWith(line));
1144
+ // Return only matching commands (empty if no match)
1145
+ return [hits, line];
1146
+ }
1147
+ /**
1148
+ * Detect execution mode based on TTY, environment, or config
1149
+ * Per spec 10_REPL_UX.md: Non-interactive mode detection
1150
+ */
1151
+ detectExecutionMode() {
1152
+ // Check config override first
1153
+ if (this.config.forceNonInteractive) {
1154
+ return 'non_interactive';
1155
+ }
1156
+ // Check environment variable (for testing)
1157
+ if (process.env.PM_RUNNER_NON_INTERACTIVE === '1') {
1158
+ return 'non_interactive';
1159
+ }
1160
+ // Primary detection: stdin TTY check
1161
+ // stdin.isTTY is undefined when piped or from heredoc
1162
+ if (!process.stdin.isTTY) {
1163
+ return 'non_interactive';
1164
+ }
1165
+ return 'interactive';
1166
+ }
1167
+ /**
1168
+ * Check if running in non-interactive mode
1169
+ */
1170
+ isNonInteractiveMode() {
1171
+ return this.executionMode === 'non_interactive';
1172
+ }
1173
+ /**
1174
+ * Get the current execution mode
1175
+ */
1176
+ getExecutionMode() {
1177
+ return this.executionMode;
1178
+ }
1179
+ /**
1180
+ * Set exit code based on current state
1181
+ * Per spec 10_REPL_UX.md: Deterministic exit codes
1182
+ */
1183
+ updateExitCode() {
1184
+ if (this.hasError) {
1185
+ this.exitCode = exports.EXIT_CODES.ERROR;
1186
+ }
1187
+ else if (this.hasIncompleteTasks) {
1188
+ this.exitCode = exports.EXIT_CODES.INCOMPLETE;
1189
+ }
1190
+ else {
1191
+ this.exitCode = exports.EXIT_CODES.COMPLETE;
1192
+ }
1193
+ }
1194
+ /**
1195
+ * Get the exit code (for non-interactive mode)
1196
+ */
1197
+ getExitCode() {
1198
+ return this.exitCode;
1199
+ }
1200
+ /**
1201
+ * Get session state (for testing)
1202
+ */
1203
+ getSessionState() {
1204
+ return { ...this.session };
1205
+ }
1206
+ /**
1207
+ * Check if running (for testing)
1208
+ */
1209
+ isRunning() {
1210
+ return this.running;
1211
+ }
1212
+ }
1213
+ exports.REPLInterface = REPLInterface;
1214
+ //# sourceMappingURL=repl-interface.js.map