ralph-research 0.1.2 → 0.1.3

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 (127) hide show
  1. package/README.md +127 -101
  2. package/dist/adapters/fs/json-file-research-project-defaults-store.d.ts +8 -0
  3. package/dist/adapters/fs/json-file-research-project-defaults-store.js +30 -0
  4. package/dist/adapters/fs/json-file-research-project-defaults-store.js.map +1 -0
  5. package/dist/adapters/fs/json-file-research-session-repository.d.ts +24 -0
  6. package/dist/adapters/fs/json-file-research-session-repository.js +199 -0
  7. package/dist/adapters/fs/json-file-research-session-repository.js.map +1 -0
  8. package/dist/adapters/fs/manifest-loader.js +8 -1
  9. package/dist/adapters/fs/manifest-loader.js.map +1 -1
  10. package/dist/adapters/proposer/codex-cli-proposer.d.ts +16 -0
  11. package/dist/adapters/proposer/codex-cli-proposer.js +106 -0
  12. package/dist/adapters/proposer/codex-cli-proposer.js.map +1 -0
  13. package/dist/adapters/proposer/codex-cli-session-driver.d.ts +64 -0
  14. package/dist/adapters/proposer/codex-cli-session-driver.js +182 -0
  15. package/dist/adapters/proposer/codex-cli-session-driver.js.map +1 -0
  16. package/dist/adapters/proposer/codex-cli-session-manager.d.ts +79 -0
  17. package/dist/adapters/proposer/codex-cli-session-manager.js +248 -0
  18. package/dist/adapters/proposer/codex-cli-session-manager.js.map +1 -0
  19. package/dist/adapters/proposer/codex-cli-session-outcome-extractor.d.ts +3 -0
  20. package/dist/adapters/proposer/codex-cli-session-outcome-extractor.js +94 -0
  21. package/dist/adapters/proposer/codex-cli-session-outcome-extractor.js.map +1 -0
  22. package/dist/adapters/proposer/proposer-factory.d.ts +22 -0
  23. package/dist/adapters/proposer/proposer-factory.js +19 -0
  24. package/dist/adapters/proposer/proposer-factory.js.map +1 -0
  25. package/dist/app/services/codex-cli-session-lifecycle-service.d.ts +116 -0
  26. package/dist/app/services/codex-cli-session-lifecycle-service.js +186 -0
  27. package/dist/app/services/codex-cli-session-lifecycle-service.js.map +1 -0
  28. package/dist/app/services/research-project-defaults-service.d.ts +18 -0
  29. package/dist/app/services/research-project-defaults-service.js +175 -0
  30. package/dist/app/services/research-project-defaults-service.js.map +1 -0
  31. package/dist/app/services/research-session-draft-service.d.ts +121 -0
  32. package/dist/app/services/research-session-draft-service.js +846 -0
  33. package/dist/app/services/research-session-draft-service.js.map +1 -0
  34. package/dist/app/services/research-session-entry-flow-summary-mapper.d.ts +12 -0
  35. package/dist/app/services/research-session-entry-flow-summary-mapper.js +33 -0
  36. package/dist/app/services/research-session-entry-flow-summary-mapper.js.map +1 -0
  37. package/dist/app/services/research-session-interactive-service.d.ts +35 -0
  38. package/dist/app/services/research-session-interactive-service.js +295 -0
  39. package/dist/app/services/research-session-interactive-service.js.map +1 -0
  40. package/dist/app/services/research-session-launch-service.d.ts +46 -0
  41. package/dist/app/services/research-session-launch-service.js +389 -0
  42. package/dist/app/services/research-session-launch-service.js.map +1 -0
  43. package/dist/app/services/research-session-orchestrator-service.d.ts +140 -0
  44. package/dist/app/services/research-session-orchestrator-service.js +614 -0
  45. package/dist/app/services/research-session-orchestrator-service.js.map +1 -0
  46. package/dist/app/services/research-session-recovery-service.d.ts +30 -0
  47. package/dist/app/services/research-session-recovery-service.js +110 -0
  48. package/dist/app/services/research-session-recovery-service.js.map +1 -0
  49. package/dist/app/services/research-session-wizard-controller.d.ts +51 -0
  50. package/dist/app/services/research-session-wizard-controller.js +220 -0
  51. package/dist/app/services/research-session-wizard-controller.js.map +1 -0
  52. package/dist/app/services/run-cycle-service.d.ts +2 -0
  53. package/dist/app/services/run-cycle-service.js +2 -0
  54. package/dist/app/services/run-cycle-service.js.map +1 -1
  55. package/dist/cli/commands/inspect.js +2 -0
  56. package/dist/cli/commands/inspect.js.map +1 -1
  57. package/dist/cli/commands/launch.d.ts +16 -0
  58. package/dist/cli/commands/launch.js +68 -0
  59. package/dist/cli/commands/launch.js.map +1 -0
  60. package/dist/cli/commands/proposer-display.d.ts +2 -0
  61. package/dist/cli/commands/proposer-display.js +18 -0
  62. package/dist/cli/commands/proposer-display.js.map +1 -0
  63. package/dist/cli/commands/resume.d.ts +14 -0
  64. package/dist/cli/commands/resume.js +134 -0
  65. package/dist/cli/commands/resume.js.map +1 -0
  66. package/dist/cli/commands/run.d.ts +1 -1
  67. package/dist/cli/commands/run.js +2 -2
  68. package/dist/cli/commands/run.js.map +1 -1
  69. package/dist/cli/commands/status.js +4 -0
  70. package/dist/cli/commands/status.js.map +1 -1
  71. package/dist/cli/main.js +2 -29
  72. package/dist/cli/main.js.map +1 -1
  73. package/dist/cli/program.d.ts +15 -0
  74. package/dist/cli/program.js +54 -0
  75. package/dist/cli/program.js.map +1 -0
  76. package/dist/cli/tui/research-session-shell.d.ts +22 -0
  77. package/dist/cli/tui/research-session-shell.js +719 -0
  78. package/dist/cli/tui/research-session-shell.js.map +1 -0
  79. package/dist/core/engine/cycle-runner.d.ts +4 -0
  80. package/dist/core/engine/cycle-runner.js +15 -9
  81. package/dist/core/engine/cycle-runner.js.map +1 -1
  82. package/dist/core/manifest/admission.d.ts +1 -0
  83. package/dist/core/manifest/admission.js +50 -0
  84. package/dist/core/manifest/admission.js.map +1 -1
  85. package/dist/core/manifest/defaults.d.ts +21 -0
  86. package/dist/core/manifest/defaults.js +21 -0
  87. package/dist/core/manifest/defaults.js.map +1 -1
  88. package/dist/core/manifest/schema.d.ts +170 -0
  89. package/dist/core/manifest/schema.js +21 -1
  90. package/dist/core/manifest/schema.js.map +1 -1
  91. package/dist/core/model/codex-cli-cycle-session.d.ts +4 -0
  92. package/dist/core/model/codex-cli-cycle-session.js +2 -0
  93. package/dist/core/model/codex-cli-cycle-session.js.map +1 -0
  94. package/dist/core/model/codex-cli-session-lifecycle.d.ts +131 -0
  95. package/dist/core/model/codex-cli-session-lifecycle.js +237 -0
  96. package/dist/core/model/codex-cli-session-lifecycle.js.map +1 -0
  97. package/dist/core/model/codex-cli-session-outcome.d.ts +121 -0
  98. package/dist/core/model/codex-cli-session-outcome.js +70 -0
  99. package/dist/core/model/codex-cli-session-outcome.js.map +1 -0
  100. package/dist/core/model/research-project-defaults.d.ts +48 -0
  101. package/dist/core/model/research-project-defaults.js +46 -0
  102. package/dist/core/model/research-project-defaults.js.map +1 -0
  103. package/dist/core/model/research-session.d.ts +1143 -0
  104. package/dist/core/model/research-session.js +689 -0
  105. package/dist/core/model/research-session.js.map +1 -0
  106. package/dist/core/model/run-record.d.ts +56 -6
  107. package/dist/core/model/run-record.js +28 -0
  108. package/dist/core/model/run-record.js.map +1 -1
  109. package/dist/core/ports/research-project-defaults-store.d.ts +5 -0
  110. package/dist/core/ports/research-project-defaults-store.js +2 -0
  111. package/dist/core/ports/research-project-defaults-store.js.map +1 -0
  112. package/dist/core/ports/research-session-repository.d.ts +25 -0
  113. package/dist/core/ports/research-session-repository.js +2 -0
  114. package/dist/core/ports/research-session-repository.js.map +1 -0
  115. package/dist/core/state/research-session-recovery-classifier.d.ts +24 -0
  116. package/dist/core/state/research-session-recovery-classifier.js +236 -0
  117. package/dist/core/state/research-session-recovery-classifier.js.map +1 -0
  118. package/dist/core/state/research-session-resume-candidate.d.ts +8 -0
  119. package/dist/core/state/research-session-resume-candidate.js +62 -0
  120. package/dist/core/state/research-session-resume-candidate.js.map +1 -0
  121. package/dist/core/state/research-session-state-machine.d.ts +62 -0
  122. package/dist/core/state/research-session-state-machine.js +443 -0
  123. package/dist/core/state/research-session-state-machine.js.map +1 -0
  124. package/dist/mcp/server.d.ts +4 -0
  125. package/dist/mcp/server.js +192 -1
  126. package/dist/mcp/server.js.map +1 -1
  127. package/package.json +1 -1
@@ -0,0 +1,846 @@
1
+ import { realpath, stat } from "node:fs/promises";
2
+ import { realpathSync, statSync } from "node:fs";
3
+ import { join, relative, resolve } from "node:path";
4
+ import { JsonFileResearchSessionRepository } from "../../adapters/fs/json-file-research-session-repository.js";
5
+ import { DEFAULT_PROJECT_BASELINE_REF, DEFAULT_STORAGE_ROOT, } from "../../core/manifest/defaults.js";
6
+ import { ResearchProjectDefaultsService } from "./research-project-defaults-service.js";
7
+ const DRAFT_STEP_ORDER = [
8
+ "permissions",
9
+ "stopRules",
10
+ "outputs",
11
+ "review",
12
+ ];
13
+ export class ResearchSessionDraftService {
14
+ now;
15
+ createRepository;
16
+ projectDefaultsService;
17
+ constructor(dependencies = {}) {
18
+ this.now = dependencies.now ?? (() => new Date());
19
+ this.createRepository =
20
+ dependencies.createRepository ??
21
+ ((sessionsRoot) => new JsonFileResearchSessionRepository(sessionsRoot));
22
+ this.projectDefaultsService =
23
+ dependencies.projectDefaultsService ??
24
+ dependencies.createProjectDefaultsService?.() ??
25
+ new ResearchProjectDefaultsService({
26
+ ...(dependencies.now ? { now: dependencies.now } : {}),
27
+ });
28
+ }
29
+ async loadDraft(input) {
30
+ const { canonicalRepoRoot, repository } = await this.resolveRepository(input.repoRoot);
31
+ const record = await repository.loadSession(input.sessionId);
32
+ return mapDraftRecord({
33
+ record,
34
+ sessionId: input.sessionId,
35
+ repoRoot: canonicalRepoRoot,
36
+ });
37
+ }
38
+ async updateDraft(input) {
39
+ const { canonicalRepoRoot, repository } = await this.resolveRepository(input.repoRoot);
40
+ const existingRecord = await repository.loadSession(input.sessionId);
41
+ const record = mapDraftRecord({
42
+ record: existingRecord,
43
+ sessionId: input.sessionId,
44
+ repoRoot: canonicalRepoRoot,
45
+ });
46
+ const nextFlowState = buildDraftFlowState({
47
+ draft: record,
48
+ patch: input.patch,
49
+ });
50
+ const nextRecord = {
51
+ ...existingRecord,
52
+ goal: resolveGoal({
53
+ patch: input.patch.goal,
54
+ current: existingRecord.goal,
55
+ }),
56
+ workingDirectory: resolveWorkingDirectoryDraft({
57
+ patch: input.patch.workingDirectory,
58
+ current: existingRecord.workingDirectory,
59
+ repoRoot: canonicalRepoRoot,
60
+ }),
61
+ context: {
62
+ ...existingRecord.context,
63
+ trackableGlobs: resolvePatternListDraft({
64
+ patch: input.patch.contextSettings?.trackableGlobs,
65
+ current: existingRecord.context.trackableGlobs,
66
+ label: "Trackable files",
67
+ }),
68
+ webSearch: resolveBooleanDraft({
69
+ patch: input.patch.contextSettings?.webSearch,
70
+ current: existingRecord.context.webSearch,
71
+ label: "Web search",
72
+ }),
73
+ shellCommandAllowlistAdditions: resolveStringListDraft({
74
+ patch: input.patch.contextSettings?.shellCommandAllowlistAdditions,
75
+ current: existingRecord.context.shellCommandAllowlistAdditions,
76
+ }),
77
+ shellCommandAllowlistRemovals: resolveStringListDraft({
78
+ patch: input.patch.contextSettings?.shellCommandAllowlistRemovals,
79
+ current: existingRecord.context.shellCommandAllowlistRemovals,
80
+ }),
81
+ },
82
+ workspace: {
83
+ ...existingRecord.workspace,
84
+ baseRef: resolveRequiredDraftString({
85
+ patch: input.patch.workspaceSettings?.baseRef,
86
+ current: existingRecord.workspace.baseRef ?? DEFAULT_PROJECT_BASELINE_REF,
87
+ label: "Baseline ref",
88
+ }),
89
+ },
90
+ agent: {
91
+ ...existingRecord.agent,
92
+ command: resolveRequiredDraftString({
93
+ patch: input.patch.agentCommand,
94
+ current: existingRecord.agent.command,
95
+ label: "Agent command",
96
+ }),
97
+ model: resolveOptionalDraftString({
98
+ patch: input.patch.agentSettings?.model,
99
+ current: existingRecord.agent.model,
100
+ }),
101
+ approvalPolicy: resolveEnumDraft({
102
+ patch: input.patch.agentSettings?.approvalPolicy,
103
+ current: existingRecord.agent.approvalPolicy,
104
+ options: ["never", "on-failure", "on-request", "untrusted"],
105
+ }),
106
+ sandboxMode: resolveEnumDraft({
107
+ patch: input.patch.agentSettings?.sandboxMode,
108
+ current: existingRecord.agent.sandboxMode,
109
+ options: ["read-only", "workspace-write", "danger-full-access"],
110
+ }),
111
+ ttySession: {
112
+ startupTimeoutSec: resolvePositiveIntegerDraft({
113
+ patch: input.patch.agentSettings?.startupTimeoutSec,
114
+ current: existingRecord.agent.ttySession.startupTimeoutSec,
115
+ label: "Agent startup timeout",
116
+ }),
117
+ turnTimeoutSec: resolvePositiveIntegerDraft({
118
+ patch: input.patch.agentSettings?.turnTimeoutSec,
119
+ current: existingRecord.agent.ttySession.turnTimeoutSec,
120
+ label: "Agent turn timeout",
121
+ }),
122
+ },
123
+ },
124
+ stopPolicy: {
125
+ repeatedFailures: resolvePositiveIntegerDraft({
126
+ patch: input.patch.stopPolicy?.repeatedFailures,
127
+ current: existingRecord.stopPolicy.repeatedFailures,
128
+ label: "Repeated failures threshold",
129
+ }),
130
+ noMeaningfulProgress: resolvePositiveIntegerDraft({
131
+ patch: input.patch.stopPolicy?.noMeaningfulProgress,
132
+ current: existingRecord.stopPolicy.noMeaningfulProgress,
133
+ label: "No-progress threshold",
134
+ }),
135
+ insufficientEvidence: resolvePositiveIntegerDraft({
136
+ patch: input.patch.stopPolicy?.insufficientEvidence,
137
+ current: existingRecord.stopPolicy.insufficientEvidence,
138
+ label: "Insufficient-evidence threshold",
139
+ }),
140
+ },
141
+ draftState: {
142
+ currentStep: input.patch.currentStep ?? record.currentStep,
143
+ completedSteps: normalizeCompletedSteps(input.patch.completedSteps ?? record.completedSteps),
144
+ returnToReview: input.patch.returnToReview ?? record.returnToReview,
145
+ reviewConfirmed: resolveReviewConfirmation({
146
+ current: record.reviewConfirmed,
147
+ currentStep: record.currentStep,
148
+ patch: input.patch,
149
+ }),
150
+ flowState: nextFlowState,
151
+ goalStep: {
152
+ goal: nextFlowState.outputs.goal,
153
+ agentCommand: nextFlowState.outputs.agentCommand,
154
+ repeatedFailures: nextFlowState.stopRules.repeatedFailures,
155
+ noMeaningfulProgress: nextFlowState.stopRules.noMeaningfulProgress,
156
+ insufficientEvidence: nextFlowState.stopRules.insufficientEvidence,
157
+ },
158
+ contextStep: {
159
+ trackableGlobs: nextFlowState.outputs.trackableGlobs,
160
+ webSearch: nextFlowState.permissions.webSearch,
161
+ shellCommandAllowlistAdditions: nextFlowState.permissions.shellCommandAllowlistAdditions,
162
+ shellCommandAllowlistRemovals: nextFlowState.permissions.shellCommandAllowlistRemovals,
163
+ },
164
+ workspaceStep: {
165
+ workingDirectory: nextFlowState.permissions.workingDirectory,
166
+ baseRef: nextFlowState.outputs.baseRef,
167
+ },
168
+ agentStep: {
169
+ command: nextFlowState.outputs.agentCommand,
170
+ model: nextFlowState.outputs.model,
171
+ approvalPolicy: nextFlowState.permissions.approvalPolicy,
172
+ sandboxMode: nextFlowState.permissions.sandboxMode,
173
+ startupTimeoutSec: nextFlowState.outputs.startupTimeoutSec,
174
+ turnTimeoutSec: nextFlowState.outputs.turnTimeoutSec,
175
+ },
176
+ },
177
+ updatedAt: this.now().toISOString(),
178
+ };
179
+ await repository.saveSession(nextRecord);
180
+ await this.projectDefaultsService.saveForSession({
181
+ repoRoot: canonicalRepoRoot,
182
+ session: nextRecord,
183
+ });
184
+ return mapDraftRecord({
185
+ record: nextRecord,
186
+ sessionId: nextRecord.sessionId,
187
+ repoRoot: canonicalRepoRoot,
188
+ });
189
+ }
190
+ async resolveRepository(repoRoot) {
191
+ const resolvedRoot = resolve(repoRoot);
192
+ const repoStats = await stat(resolvedRoot).catch(() => null);
193
+ if (!repoStats?.isDirectory()) {
194
+ throw new Error(`Working directory is not a directory: ${resolvedRoot}`);
195
+ }
196
+ const canonicalRepoRoot = await realpath(resolvedRoot);
197
+ const storageRoot = join(canonicalRepoRoot, DEFAULT_STORAGE_ROOT);
198
+ const sessionsRoot = join(storageRoot, "sessions");
199
+ return {
200
+ canonicalRepoRoot,
201
+ repository: this.createRepository(sessionsRoot),
202
+ };
203
+ }
204
+ }
205
+ export function validateGoalStepDraft(draft) {
206
+ const fieldErrors = {};
207
+ captureValidationError(() => normalizeRequiredString(draft.goal, "Goal"), (message) => {
208
+ fieldErrors.goal = message;
209
+ });
210
+ captureValidationError(() => normalizePositiveInteger(draft.stopPolicy.repeatedFailures, "Repeated failures threshold"), (message) => {
211
+ fieldErrors.repeatedFailures = message;
212
+ });
213
+ captureValidationError(() => normalizePositiveInteger(draft.stopPolicy.noMeaningfulProgress, "No-progress threshold"), (message) => {
214
+ fieldErrors.noMeaningfulProgress = message;
215
+ });
216
+ captureValidationError(() => normalizePositiveInteger(draft.stopPolicy.insufficientEvidence, "Insufficient-evidence threshold"), (message) => {
217
+ fieldErrors.insufficientEvidence = message;
218
+ });
219
+ return {
220
+ isValid: Object.keys(fieldErrors).length === 0,
221
+ fieldErrors,
222
+ };
223
+ }
224
+ export function validateContextStepDraft(draft) {
225
+ const fieldErrors = {};
226
+ captureValidationError(() => normalizePatternList(draft.contextSettings.trackableGlobs, "Trackable files"), (message) => {
227
+ fieldErrors.trackableGlobs = message;
228
+ });
229
+ captureValidationError(() => normalizeBooleanChoice(draft.contextSettings.webSearch, "Web search"), (message) => {
230
+ fieldErrors.webSearch = message;
231
+ });
232
+ captureValidationError(() => normalizeOptionalStringList(draft.contextSettings.shellCommandAllowlistAdditions, "Shell allowlist additions"), (message) => {
233
+ fieldErrors.shellCommandAllowlistAdditions = message;
234
+ });
235
+ captureValidationError(() => normalizeOptionalStringList(draft.contextSettings.shellCommandAllowlistRemovals, "Shell allowlist removals"), (message) => {
236
+ fieldErrors.shellCommandAllowlistRemovals = message;
237
+ });
238
+ return {
239
+ isValid: Object.keys(fieldErrors).length === 0,
240
+ fieldErrors,
241
+ };
242
+ }
243
+ export function validateWorkspaceStepDraft(draft) {
244
+ const fieldErrors = {};
245
+ captureValidationError(() => normalizeWorkspaceDirectory(draft.workingDirectory, "Working directory", draft.repoRoot), (message) => {
246
+ fieldErrors.workingDirectory = message;
247
+ });
248
+ captureValidationError(() => normalizeRequiredString(draft.workspaceSettings.baseRef, "Baseline ref"), (message) => {
249
+ fieldErrors.baseRef = message;
250
+ });
251
+ return {
252
+ isValid: Object.keys(fieldErrors).length === 0,
253
+ fieldErrors,
254
+ };
255
+ }
256
+ export function validateAgentStepDraft(draft) {
257
+ const fieldErrors = {};
258
+ captureValidationError(() => normalizeRequiredString(draft.agentCommand, "Agent command"), (message) => {
259
+ fieldErrors.agentCommand = message;
260
+ });
261
+ captureValidationError(() => normalizeEnum(draft.agentSettings.approvalPolicy, ["never", "on-failure", "on-request", "untrusted"], "Approval policy"), (message) => {
262
+ fieldErrors.approvalPolicy = message;
263
+ });
264
+ captureValidationError(() => normalizeEnum(draft.agentSettings.sandboxMode, ["read-only", "workspace-write", "danger-full-access"], "Sandbox mode"), (message) => {
265
+ fieldErrors.sandboxMode = message;
266
+ });
267
+ captureValidationError(() => normalizePositiveInteger(draft.agentSettings.startupTimeoutSec, "Agent startup timeout"), (message) => {
268
+ fieldErrors.startupTimeoutSec = message;
269
+ });
270
+ captureValidationError(() => normalizePositiveInteger(draft.agentSettings.turnTimeoutSec, "Agent turn timeout"), (message) => {
271
+ fieldErrors.turnTimeoutSec = message;
272
+ });
273
+ return {
274
+ isValid: Object.keys(fieldErrors).length === 0,
275
+ fieldErrors,
276
+ };
277
+ }
278
+ export function validatePermissionsStepDraft(draft) {
279
+ const workspaceValidation = validateWorkspaceStepDraft(draft);
280
+ const contextValidation = validateContextStepDraft(draft);
281
+ const agentValidation = validateAgentStepDraft(draft);
282
+ const fieldErrors = {
283
+ ...pickValidationFields(workspaceValidation, ["workingDirectory"]),
284
+ ...pickValidationFields(contextValidation, [
285
+ "webSearch",
286
+ "shellCommandAllowlistAdditions",
287
+ "shellCommandAllowlistRemovals",
288
+ ]),
289
+ ...pickValidationFields(agentValidation, ["approvalPolicy", "sandboxMode"]),
290
+ };
291
+ return {
292
+ isValid: Object.keys(fieldErrors).length === 0,
293
+ fieldErrors,
294
+ };
295
+ }
296
+ export function validateStopRulesStepDraft(draft) {
297
+ const goalValidation = validateGoalStepDraft(draft);
298
+ const fieldErrors = pickValidationFields(goalValidation, [
299
+ "repeatedFailures",
300
+ "noMeaningfulProgress",
301
+ "insufficientEvidence",
302
+ ]);
303
+ return {
304
+ isValid: Object.keys(fieldErrors).length === 0,
305
+ fieldErrors,
306
+ };
307
+ }
308
+ export function validateOutputsStepDraft(draft) {
309
+ const goalValidation = validateGoalStepDraft(draft);
310
+ const workspaceValidation = validateWorkspaceStepDraft(draft);
311
+ const contextValidation = validateContextStepDraft(draft);
312
+ const agentValidation = validateAgentStepDraft(draft);
313
+ const fieldErrors = {
314
+ ...pickValidationFields(goalValidation, ["goal"]),
315
+ ...pickValidationFields(workspaceValidation, ["baseRef"]),
316
+ ...pickValidationFields(contextValidation, ["trackableGlobs"]),
317
+ ...pickValidationFields(agentValidation, ["agentCommand", "startupTimeoutSec", "turnTimeoutSec"]),
318
+ };
319
+ return {
320
+ isValid: Object.keys(fieldErrors).length === 0,
321
+ fieldErrors,
322
+ };
323
+ }
324
+ export function validateReviewStepDraft(draft) {
325
+ const validations = [
326
+ validatePermissionsStepDraft(draft),
327
+ validateStopRulesStepDraft(draft),
328
+ validateOutputsStepDraft(draft),
329
+ ];
330
+ const fieldErrors = validations.reduce((combined, validation) => ({
331
+ ...combined,
332
+ ...validation.fieldErrors,
333
+ }), {});
334
+ return {
335
+ isValid: Object.keys(fieldErrors).length === 0,
336
+ fieldErrors,
337
+ };
338
+ }
339
+ export function buildResearchSessionReviewSummary(draft) {
340
+ return draft.reviewState.sections.map((section) => ({
341
+ ...section,
342
+ validation: validateResearchSessionReviewSection(draft, section.step),
343
+ }));
344
+ }
345
+ export function buildResearchSessionReviewState(draft) {
346
+ return buildResearchSessionReviewStateFromValues({
347
+ workingDirectory: draft.workingDirectory,
348
+ webSearch: draft.contextSettings.webSearch,
349
+ shellCommandAllowlistAdditions: draft.contextSettings.shellCommandAllowlistAdditions,
350
+ shellCommandAllowlistRemovals: draft.contextSettings.shellCommandAllowlistRemovals,
351
+ approvalPolicy: draft.agentSettings.approvalPolicy,
352
+ sandboxMode: draft.agentSettings.sandboxMode,
353
+ repeatedFailures: draft.stopPolicy.repeatedFailures,
354
+ noMeaningfulProgress: draft.stopPolicy.noMeaningfulProgress,
355
+ insufficientEvidence: draft.stopPolicy.insufficientEvidence,
356
+ goal: draft.goal,
357
+ trackableGlobs: draft.contextSettings.trackableGlobs,
358
+ baseRef: draft.workspaceSettings.baseRef,
359
+ agentCommand: draft.agentCommand,
360
+ model: draft.agentSettings.model,
361
+ startupTimeoutSec: draft.agentSettings.startupTimeoutSec,
362
+ turnTimeoutSec: draft.agentSettings.turnTimeoutSec,
363
+ });
364
+ }
365
+ export function buildResearchSessionReviewStateFromValues(input) {
366
+ return {
367
+ sections: [
368
+ {
369
+ index: "1",
370
+ label: "Permissions",
371
+ step: "permissions",
372
+ fields: [
373
+ { label: "Working directory", value: input.workingDirectory },
374
+ { label: "Web search", value: input.webSearch },
375
+ { label: "Shell allowlist additions", value: input.shellCommandAllowlistAdditions },
376
+ { label: "Shell allowlist removals", value: input.shellCommandAllowlistRemovals },
377
+ { label: "Approval policy", value: input.approvalPolicy },
378
+ { label: "Sandbox mode", value: input.sandboxMode },
379
+ ],
380
+ },
381
+ {
382
+ index: "2",
383
+ label: "Stop Rules",
384
+ step: "stopRules",
385
+ fields: [
386
+ { label: "Repeated failures threshold", value: input.repeatedFailures },
387
+ { label: "No-progress threshold", value: input.noMeaningfulProgress },
388
+ { label: "Insufficient-evidence threshold", value: input.insufficientEvidence },
389
+ ],
390
+ },
391
+ {
392
+ index: "3",
393
+ label: "Outputs",
394
+ step: "outputs",
395
+ fields: [
396
+ { label: "Goal", value: input.goal },
397
+ { label: "Trackable files", value: input.trackableGlobs },
398
+ { label: "Baseline ref", value: input.baseRef },
399
+ { label: "Agent command", value: input.agentCommand },
400
+ { label: "Model override", value: input.model },
401
+ { label: "Startup timeout (sec)", value: input.startupTimeoutSec },
402
+ { label: "Turn timeout (sec)", value: input.turnTimeoutSec },
403
+ ],
404
+ },
405
+ ],
406
+ };
407
+ }
408
+ function mapDraftRecord(input) {
409
+ if (!input.record) {
410
+ throw new Error(`Draft session not found: ${input.sessionId}`);
411
+ }
412
+ if (input.record.status !== "draft") {
413
+ throw new Error(`Session ${input.sessionId} is not editable from the launch draft TUI`);
414
+ }
415
+ const flowState = resolveDraftFlowState(input.record);
416
+ return {
417
+ sessionId: input.record.sessionId,
418
+ currentStep: normalizeDraftStep(input.record.draftState?.currentStep),
419
+ completedSteps: resolveCompletedSteps({
420
+ completedSteps: input.record.draftState?.completedSteps,
421
+ currentStep: input.record.draftState?.currentStep,
422
+ }),
423
+ returnToReview: input.record.draftState?.returnToReview ?? false,
424
+ reviewConfirmed: input.record.draftState?.reviewConfirmed ?? false,
425
+ goal: flowState.outputs.goal,
426
+ repoRoot: input.repoRoot,
427
+ contextSettings: {
428
+ trackableGlobs: flowState.outputs.trackableGlobs,
429
+ webSearch: flowState.permissions.webSearch,
430
+ shellCommandAllowlistAdditions: flowState.permissions.shellCommandAllowlistAdditions,
431
+ shellCommandAllowlistRemovals: flowState.permissions.shellCommandAllowlistRemovals,
432
+ },
433
+ workingDirectory: flowState.permissions.workingDirectory,
434
+ workspaceSettings: {
435
+ baseRef: flowState.outputs.baseRef,
436
+ },
437
+ agentCommand: flowState.outputs.agentCommand,
438
+ stopPolicy: {
439
+ repeatedFailures: flowState.stopRules.repeatedFailures,
440
+ noMeaningfulProgress: flowState.stopRules.noMeaningfulProgress,
441
+ insufficientEvidence: flowState.stopRules.insufficientEvidence,
442
+ },
443
+ agentSettings: {
444
+ model: flowState.outputs.model,
445
+ approvalPolicy: flowState.permissions.approvalPolicy,
446
+ sandboxMode: flowState.permissions.sandboxMode,
447
+ startupTimeoutSec: flowState.outputs.startupTimeoutSec,
448
+ turnTimeoutSec: flowState.outputs.turnTimeoutSec,
449
+ },
450
+ reviewState: flowState.review,
451
+ };
452
+ }
453
+ function buildDraftFlowState(input) {
454
+ const baseFlowState = {
455
+ permissions: {
456
+ workingDirectory: input.patch.workingDirectory ?? input.draft.workingDirectory,
457
+ webSearch: input.patch.contextSettings?.webSearch ?? input.draft.contextSettings.webSearch,
458
+ shellCommandAllowlistAdditions: input.patch.contextSettings?.shellCommandAllowlistAdditions ??
459
+ input.draft.contextSettings.shellCommandAllowlistAdditions,
460
+ shellCommandAllowlistRemovals: input.patch.contextSettings?.shellCommandAllowlistRemovals ??
461
+ input.draft.contextSettings.shellCommandAllowlistRemovals,
462
+ approvalPolicy: input.patch.agentSettings?.approvalPolicy ?? input.draft.agentSettings.approvalPolicy,
463
+ sandboxMode: input.patch.agentSettings?.sandboxMode ?? input.draft.agentSettings.sandboxMode,
464
+ },
465
+ stopRules: {
466
+ repeatedFailures: input.patch.stopPolicy?.repeatedFailures ?? input.draft.stopPolicy.repeatedFailures,
467
+ noMeaningfulProgress: input.patch.stopPolicy?.noMeaningfulProgress ?? input.draft.stopPolicy.noMeaningfulProgress,
468
+ insufficientEvidence: input.patch.stopPolicy?.insufficientEvidence ?? input.draft.stopPolicy.insufficientEvidence,
469
+ },
470
+ outputs: {
471
+ goal: input.patch.goal ?? input.draft.goal,
472
+ trackableGlobs: input.patch.contextSettings?.trackableGlobs ?? input.draft.contextSettings.trackableGlobs,
473
+ baseRef: input.patch.workspaceSettings?.baseRef ?? input.draft.workspaceSettings.baseRef,
474
+ agentCommand: input.patch.agentCommand ?? input.draft.agentCommand,
475
+ model: input.patch.agentSettings?.model ?? input.draft.agentSettings.model,
476
+ startupTimeoutSec: input.patch.agentSettings?.startupTimeoutSec ?? input.draft.agentSettings.startupTimeoutSec,
477
+ turnTimeoutSec: input.patch.agentSettings?.turnTimeoutSec ?? input.draft.agentSettings.turnTimeoutSec,
478
+ },
479
+ };
480
+ return {
481
+ ...baseFlowState,
482
+ review: buildResearchSessionReviewStateFromValues({
483
+ workingDirectory: baseFlowState.permissions.workingDirectory,
484
+ webSearch: baseFlowState.permissions.webSearch,
485
+ shellCommandAllowlistAdditions: baseFlowState.permissions.shellCommandAllowlistAdditions,
486
+ shellCommandAllowlistRemovals: baseFlowState.permissions.shellCommandAllowlistRemovals,
487
+ approvalPolicy: baseFlowState.permissions.approvalPolicy,
488
+ sandboxMode: baseFlowState.permissions.sandboxMode,
489
+ repeatedFailures: baseFlowState.stopRules.repeatedFailures,
490
+ noMeaningfulProgress: baseFlowState.stopRules.noMeaningfulProgress,
491
+ insufficientEvidence: baseFlowState.stopRules.insufficientEvidence,
492
+ goal: baseFlowState.outputs.goal,
493
+ trackableGlobs: baseFlowState.outputs.trackableGlobs,
494
+ baseRef: baseFlowState.outputs.baseRef,
495
+ agentCommand: baseFlowState.outputs.agentCommand,
496
+ model: baseFlowState.outputs.model,
497
+ startupTimeoutSec: baseFlowState.outputs.startupTimeoutSec,
498
+ turnTimeoutSec: baseFlowState.outputs.turnTimeoutSec,
499
+ }),
500
+ };
501
+ }
502
+ function resolveReviewConfirmation(input) {
503
+ if (input.patch.reviewConfirmed !== undefined) {
504
+ return input.patch.reviewConfirmed;
505
+ }
506
+ if (input.patch.currentStep !== undefined && input.patch.currentStep !== input.currentStep) {
507
+ return false;
508
+ }
509
+ if (patchTouchesReviewInputs(input.patch)) {
510
+ return false;
511
+ }
512
+ return input.current;
513
+ }
514
+ function patchTouchesReviewInputs(patch) {
515
+ if (patch.goal !== undefined || patch.workingDirectory !== undefined || patch.agentCommand !== undefined) {
516
+ return true;
517
+ }
518
+ if (patch.contextSettings && Object.values(patch.contextSettings).some((value) => value !== undefined)) {
519
+ return true;
520
+ }
521
+ if (patch.workspaceSettings && Object.values(patch.workspaceSettings).some((value) => value !== undefined)) {
522
+ return true;
523
+ }
524
+ if (patch.stopPolicy && Object.values(patch.stopPolicy).some((value) => value !== undefined)) {
525
+ return true;
526
+ }
527
+ if (patch.agentSettings && Object.values(patch.agentSettings).some((value) => value !== undefined)) {
528
+ return true;
529
+ }
530
+ return false;
531
+ }
532
+ function resolveDraftFlowState(record) {
533
+ const permissions = record.draftState?.flowState?.permissions;
534
+ const stopRules = record.draftState?.flowState?.stopRules;
535
+ const outputs = record.draftState?.flowState?.outputs;
536
+ const baseFlowState = {
537
+ permissions: {
538
+ workingDirectory: permissions?.workingDirectory ??
539
+ record.draftState?.workspaceStep?.workingDirectory ??
540
+ record.workingDirectory,
541
+ webSearch: permissions?.webSearch ??
542
+ record.draftState?.contextStep?.webSearch ??
543
+ formatBooleanChoice(record.context.webSearch),
544
+ shellCommandAllowlistAdditions: permissions?.shellCommandAllowlistAdditions ??
545
+ record.draftState?.contextStep?.shellCommandAllowlistAdditions ??
546
+ record.context.shellCommandAllowlistAdditions.join(", "),
547
+ shellCommandAllowlistRemovals: permissions?.shellCommandAllowlistRemovals ??
548
+ record.draftState?.contextStep?.shellCommandAllowlistRemovals ??
549
+ record.context.shellCommandAllowlistRemovals.join(", "),
550
+ approvalPolicy: permissions?.approvalPolicy ??
551
+ record.draftState?.agentStep?.approvalPolicy ??
552
+ record.agent.approvalPolicy,
553
+ sandboxMode: permissions?.sandboxMode ??
554
+ record.draftState?.agentStep?.sandboxMode ??
555
+ record.agent.sandboxMode,
556
+ },
557
+ stopRules: {
558
+ repeatedFailures: stopRules?.repeatedFailures ??
559
+ record.draftState?.goalStep?.repeatedFailures ??
560
+ String(record.stopPolicy.repeatedFailures),
561
+ noMeaningfulProgress: stopRules?.noMeaningfulProgress ??
562
+ record.draftState?.goalStep?.noMeaningfulProgress ??
563
+ String(record.stopPolicy.noMeaningfulProgress),
564
+ insufficientEvidence: stopRules?.insufficientEvidence ??
565
+ record.draftState?.goalStep?.insufficientEvidence ??
566
+ String(record.stopPolicy.insufficientEvidence),
567
+ },
568
+ outputs: {
569
+ goal: outputs?.goal ?? record.draftState?.goalStep?.goal ?? record.goal,
570
+ trackableGlobs: outputs?.trackableGlobs ??
571
+ record.draftState?.contextStep?.trackableGlobs ??
572
+ record.context.trackableGlobs.join(", "),
573
+ baseRef: outputs?.baseRef ??
574
+ record.draftState?.workspaceStep?.baseRef ??
575
+ record.workspace.baseRef ??
576
+ DEFAULT_PROJECT_BASELINE_REF,
577
+ agentCommand: outputs?.agentCommand ??
578
+ record.draftState?.goalStep?.agentCommand ??
579
+ record.draftState?.agentStep?.command ??
580
+ record.agent.command,
581
+ model: outputs?.model ?? record.draftState?.agentStep?.model ?? record.agent.model ?? "",
582
+ startupTimeoutSec: outputs?.startupTimeoutSec ??
583
+ record.draftState?.agentStep?.startupTimeoutSec ??
584
+ String(record.agent.ttySession.startupTimeoutSec),
585
+ turnTimeoutSec: outputs?.turnTimeoutSec ??
586
+ record.draftState?.agentStep?.turnTimeoutSec ??
587
+ String(record.agent.ttySession.turnTimeoutSec),
588
+ },
589
+ };
590
+ return {
591
+ ...baseFlowState,
592
+ review: record.draftState?.flowState?.review ??
593
+ buildResearchSessionReviewStateFromValues({
594
+ workingDirectory: baseFlowState.permissions.workingDirectory,
595
+ webSearch: baseFlowState.permissions.webSearch,
596
+ shellCommandAllowlistAdditions: baseFlowState.permissions.shellCommandAllowlistAdditions,
597
+ shellCommandAllowlistRemovals: baseFlowState.permissions.shellCommandAllowlistRemovals,
598
+ approvalPolicy: baseFlowState.permissions.approvalPolicy,
599
+ sandboxMode: baseFlowState.permissions.sandboxMode,
600
+ repeatedFailures: baseFlowState.stopRules.repeatedFailures,
601
+ noMeaningfulProgress: baseFlowState.stopRules.noMeaningfulProgress,
602
+ insufficientEvidence: baseFlowState.stopRules.insufficientEvidence,
603
+ goal: baseFlowState.outputs.goal,
604
+ trackableGlobs: baseFlowState.outputs.trackableGlobs,
605
+ baseRef: baseFlowState.outputs.baseRef,
606
+ agentCommand: baseFlowState.outputs.agentCommand,
607
+ model: baseFlowState.outputs.model,
608
+ startupTimeoutSec: baseFlowState.outputs.startupTimeoutSec,
609
+ turnTimeoutSec: baseFlowState.outputs.turnTimeoutSec,
610
+ }),
611
+ };
612
+ }
613
+ function normalizeDraftStep(value) {
614
+ if (value === "permissions") {
615
+ return "permissions";
616
+ }
617
+ if (value === "stopRules") {
618
+ return "stopRules";
619
+ }
620
+ if (value === "outputs") {
621
+ return "outputs";
622
+ }
623
+ if (value === "review") {
624
+ return "review";
625
+ }
626
+ if (value === "agent") {
627
+ return "outputs";
628
+ }
629
+ return "permissions";
630
+ }
631
+ function resolveCompletedSteps(input) {
632
+ if (input.completedSteps && input.completedSteps.length > 0) {
633
+ return normalizeCompletedSteps(input.completedSteps);
634
+ }
635
+ const currentStep = normalizeDraftStep(input.currentStep);
636
+ const currentIndex = DRAFT_STEP_ORDER.indexOf(currentStep);
637
+ if (currentIndex <= 0) {
638
+ return [];
639
+ }
640
+ return DRAFT_STEP_ORDER.slice(0, currentIndex);
641
+ }
642
+ function normalizeCompletedSteps(value) {
643
+ const completed = new Set(value.map((step) => normalizeDraftStep(step)));
644
+ return DRAFT_STEP_ORDER.filter((step) => completed.has(step));
645
+ }
646
+ function pickValidationFields(validation, fields) {
647
+ const fieldErrors = {};
648
+ for (const field of fields) {
649
+ const message = validation.fieldErrors[field];
650
+ if (message) {
651
+ fieldErrors[field] = message;
652
+ }
653
+ }
654
+ return fieldErrors;
655
+ }
656
+ function validateResearchSessionReviewSection(draft, step) {
657
+ switch (step) {
658
+ case "permissions":
659
+ return validatePermissionsStepDraft(draft);
660
+ case "stopRules":
661
+ return validateStopRulesStepDraft(draft);
662
+ case "outputs":
663
+ return validateOutputsStepDraft(draft);
664
+ }
665
+ }
666
+ function normalizeRequiredString(value, label) {
667
+ const normalized = value.trim();
668
+ if (!normalized) {
669
+ throw new Error(`${label} is required`);
670
+ }
671
+ return normalized;
672
+ }
673
+ function normalizeWorkspaceDirectory(value, label, repoRoot) {
674
+ const normalized = normalizeRequiredString(value, label);
675
+ const resolvedPath = resolve(repoRoot, normalized);
676
+ let directoryStats;
677
+ try {
678
+ directoryStats = statSync(resolvedPath);
679
+ }
680
+ catch {
681
+ throw new Error(`${label} does not exist`);
682
+ }
683
+ if (!directoryStats.isDirectory()) {
684
+ throw new Error(`${label} is not a directory`);
685
+ }
686
+ const repoRealPath = realpathSync(repoRoot);
687
+ const directoryRealPath = realpathSync(resolvedPath);
688
+ const repoRelativePath = relative(repoRealPath, directoryRealPath);
689
+ if (repoRelativePath.startsWith("..") || repoRelativePath === "..") {
690
+ throw new Error(`${label} must stay within the repo root`);
691
+ }
692
+ return directoryRealPath;
693
+ }
694
+ function normalizePatternList(value, label) {
695
+ const normalizedPatterns = value
696
+ .split(/\r?\n|,/)
697
+ .map((entry) => entry.trim())
698
+ .filter((entry) => entry.length > 0);
699
+ if (normalizedPatterns.length === 0) {
700
+ throw new Error(`${label} must include at least one pattern`);
701
+ }
702
+ return normalizedPatterns.map((pattern) => normalizeWorkspaceRelativePattern(pattern, label));
703
+ }
704
+ function normalizeWorkspaceRelativePattern(value, label) {
705
+ if (value.startsWith("/") || value.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(value)) {
706
+ throw new Error(`${label} must stay within the working directory`);
707
+ }
708
+ const segments = value.split(/[\\/]+/).filter((segment) => segment.length > 0 && segment !== ".");
709
+ if (segments.length === 0 || segments.includes("..")) {
710
+ throw new Error(`${label} must stay within the working directory`);
711
+ }
712
+ return value;
713
+ }
714
+ function normalizeOptionalStringList(value, _label) {
715
+ return value
716
+ .split(/\r?\n|,/)
717
+ .map((entry) => entry.trim())
718
+ .filter((entry) => entry.length > 0);
719
+ }
720
+ function normalizePositiveInteger(value, label) {
721
+ const parsed = Number.parseInt(value.trim(), 10);
722
+ if (!Number.isInteger(parsed) || parsed < 1) {
723
+ throw new Error(`${label} must be a positive integer`);
724
+ }
725
+ return parsed;
726
+ }
727
+ function normalizeEnum(value, options, label) {
728
+ const normalized = value.trim();
729
+ const match = options.find((option) => option === normalized);
730
+ if (!match) {
731
+ throw new Error(`${label} must be one of: ${options.join(", ")}`);
732
+ }
733
+ return match;
734
+ }
735
+ function normalizeBooleanChoice(value, label) {
736
+ const normalized = value.trim().toLowerCase();
737
+ if (["enabled", "true", "yes", "on"].includes(normalized)) {
738
+ return true;
739
+ }
740
+ if (["disabled", "false", "no", "off"].includes(normalized)) {
741
+ return false;
742
+ }
743
+ throw new Error(`${label} must be enabled or disabled`);
744
+ }
745
+ function formatBooleanChoice(value) {
746
+ return value ? "enabled" : "disabled";
747
+ }
748
+ function resolveGoal(input) {
749
+ if (input.patch === undefined) {
750
+ return input.current;
751
+ }
752
+ try {
753
+ return normalizeRequiredString(input.patch, "Goal");
754
+ }
755
+ catch {
756
+ return input.current;
757
+ }
758
+ }
759
+ function resolveWorkingDirectoryDraft(input) {
760
+ if (input.patch === undefined) {
761
+ return input.current;
762
+ }
763
+ try {
764
+ return normalizeWorkspaceDirectory(input.patch, "Working directory", input.repoRoot);
765
+ }
766
+ catch {
767
+ return input.current;
768
+ }
769
+ }
770
+ function resolveRequiredDraftString(input) {
771
+ if (input.patch === undefined) {
772
+ return input.current;
773
+ }
774
+ try {
775
+ return normalizeRequiredString(input.patch, input.label);
776
+ }
777
+ catch {
778
+ return input.current;
779
+ }
780
+ }
781
+ function resolveOptionalDraftString(input) {
782
+ if (input.patch === undefined) {
783
+ return input.current;
784
+ }
785
+ const normalized = input.patch.trim();
786
+ return normalized ? normalized : undefined;
787
+ }
788
+ function resolvePositiveIntegerDraft(input) {
789
+ if (input.patch === undefined) {
790
+ return input.current;
791
+ }
792
+ try {
793
+ return normalizePositiveInteger(input.patch, input.label);
794
+ }
795
+ catch {
796
+ return input.current;
797
+ }
798
+ }
799
+ function resolveEnumDraft(input) {
800
+ if (input.patch === undefined) {
801
+ return input.current;
802
+ }
803
+ try {
804
+ return normalizeEnum(input.patch, input.options, "Selection");
805
+ }
806
+ catch {
807
+ return input.current;
808
+ }
809
+ }
810
+ function resolvePatternListDraft(input) {
811
+ if (input.patch === undefined) {
812
+ return input.current;
813
+ }
814
+ try {
815
+ return normalizePatternList(input.patch, input.label);
816
+ }
817
+ catch {
818
+ return input.current;
819
+ }
820
+ }
821
+ function resolveStringListDraft(input) {
822
+ if (input.patch === undefined) {
823
+ return input.current;
824
+ }
825
+ return normalizeOptionalStringList(input.patch, "List");
826
+ }
827
+ function resolveBooleanDraft(input) {
828
+ if (input.patch === undefined) {
829
+ return input.current;
830
+ }
831
+ try {
832
+ return normalizeBooleanChoice(input.patch, input.label);
833
+ }
834
+ catch {
835
+ return input.current;
836
+ }
837
+ }
838
+ function captureValidationError(validate, onError) {
839
+ try {
840
+ validate();
841
+ }
842
+ catch (error) {
843
+ onError(error instanceof Error ? error.message : "Validation failed");
844
+ }
845
+ }
846
+ //# sourceMappingURL=research-session-draft-service.js.map