@xfey/tutti 0.1.49 → 0.1.51

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 (60) hide show
  1. package/dist/approvals/projections.d.ts +0 -1
  2. package/dist/approvals/projections.js +0 -1
  3. package/dist/artifacts/candidate-validator.js +3 -3
  4. package/dist/artifacts/manifest.d.ts +2 -0
  5. package/dist/artifacts/manifest.js +9 -2
  6. package/dist/artifacts/preview-runtime.js +1 -0
  7. package/dist/collaboration-state/index.d.ts +1 -1
  8. package/dist/collaboration-state/index.js +1 -1
  9. package/dist/collaboration-state/recovery.d.ts +2 -1
  10. package/dist/collaboration-state/recovery.js +16 -0
  11. package/dist/collaboration-state/types.d.ts +5 -0
  12. package/dist/control-plane/follow-up-run.js +0 -3
  13. package/dist/control-plane/formatters.d.ts +2 -4
  14. package/dist/control-plane/formatters.js +5 -11
  15. package/dist/control-plane/index.d.ts +1 -1
  16. package/dist/control-plane/run-result-recording.d.ts +2 -9
  17. package/dist/control-plane/run-result-recording.js +6 -24
  18. package/dist/control-plane/run-scheduler.js +0 -3
  19. package/dist/control-plane/run-start.js +1 -8
  20. package/dist/control-plane/startup-recovery.d.ts +1 -1
  21. package/dist/control-plane/startup-recovery.js +90 -23
  22. package/dist/control-plane/types.d.ts +4 -17
  23. package/dist/providers/openai/app-server/workspace-write-run.d.ts +0 -2
  24. package/dist/providers/openai/app-server/workspace-write-run.js +0 -1
  25. package/dist/run-pipeline/candidate-diff.js +1 -7
  26. package/dist/run-pipeline/openai.d.ts +0 -4
  27. package/dist/run-pipeline/openai.js +42 -148
  28. package/dist/run-pipeline/task-run-invocation.d.ts +1 -2
  29. package/dist/run-pipeline/task-run-invocation.js +2 -4
  30. package/dist/server-shell/cli/host-server-runtime.js +3 -10
  31. package/dist/store/lifecycle.d.ts +1 -0
  32. package/dist/store/lifecycle.js +8 -6
  33. package/dist/workspace-ops/index.d.ts +3 -2
  34. package/dist/workspace-ops/index.js +2 -1
  35. package/dist/workspace-ops/mainline.d.ts +1 -1
  36. package/dist/workspace-ops/mainline.js +1 -5
  37. package/dist/workspace-ops/project-docs.js +26 -15
  38. package/dist/workspace-ops/project-workspace.d.ts +12 -0
  39. package/dist/workspace-ops/project-workspace.js +257 -0
  40. package/dist/workspace-ops/reference-files.js +9 -9
  41. package/dist/workspace-ops/reference-summaries.js +9 -7
  42. package/dist/workspace-ops/system-commits.d.ts +7 -0
  43. package/dist/workspace-ops/system-commits.js +43 -0
  44. package/dist/workspace-ops/types.d.ts +22 -41
  45. package/package.json +1 -1
  46. package/prompts/README.md +2 -2
  47. package/prompts/prompt-flow-experiment-plan.md +5 -6
  48. package/prompts/runs/README.md +5 -3
  49. package/prompts/runs/task-continuation.md +6 -4
  50. package/prompts/runs/task-retry.md +11 -10
  51. package/prompts/runs/task-run.md +6 -4
  52. package/web/assets/{homepage-motion-scene-CKLsNPVv.js → homepage-motion-scene-CV9ceFzS.js} +1 -1
  53. package/web/assets/index-CxVBmEoi.js +70 -0
  54. package/web/assets/{index-DQogRzu_.css → index-DNnf10zz.css} +1 -1
  55. package/web/index.html +2 -2
  56. package/dist/run-pipeline/promotion-reconcile.d.ts +0 -21
  57. package/dist/run-pipeline/promotion-reconcile.js +0 -195
  58. package/dist/workspace-ops/run-workspaces.d.ts +0 -19
  59. package/dist/workspace-ops/run-workspaces.js +0 -302
  60. package/web/assets/index-CkXOC6nC.js +0 -69
@@ -1,15 +1,13 @@
1
- import { join } from "node:path";
2
1
  import { createNodeNpmCheckPlan, executeCheckPlan } from "../checks/index.js";
3
2
  import { candidateChangedAfterChecks } from "./candidate-diff.js";
4
3
  import { runArtifactCandidateGate, } from "./artifact-applicability.js";
5
4
  import { checkSkipFields, checkStatus, checksSummary, completedWithoutRepoChanges, docsStatusForChangedPaths, failedBeforeChecks, failedFinishedResult, promotionNotAttempted, promotionNotPromoted, successfulRunSummary, } from "./result-projections.js";
6
- import { runPromotionReconcile } from "./promotion-reconcile.js";
7
5
  import { runWorkspaceWriteTask } from "./task-run-invocation.js";
8
6
  import { readOpenAiApiKeyCredential } from "../providers/openai/credential-store.js";
9
7
  import { resolveOpenAiProviderConfig, } from "../providers/openai/provider-config.js";
10
8
  import { CodexAppServerWorkspaceWriteRunError } from "../providers/openai/app-server/workspace-write-run.js";
11
9
  import { getProjectUserSkillsRoot } from "../skills/index.js";
12
- import { cleanupRunWorkspace, collectRunDiff, createResultCommit, prepareRunWorkspace, promoteRunResult, WorkspaceOpsError, } from "../workspace-ops/index.js";
10
+ import { collectProjectSourceDiff, createAcceptedCheckpoint, inspectProjectWorkspace, WorkspaceOpsError, } from "../workspace-ops/index.js";
13
11
  export { TASK_RUN_OUTPUT_SCHEMA } from "./task-run-output.js";
14
12
  function isRecord(value) {
15
13
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -55,12 +53,6 @@ function summarizeWorkspaceOpsError(error, changedPaths) {
55
53
  : promotionNotAttempted("workspace_ops_failed", error.message),
56
54
  });
57
55
  }
58
- function isMainlineMovedPromotionConflict(error) {
59
- return (error instanceof WorkspaceOpsError &&
60
- error.code === "conflict" &&
61
- isRecord(error.details) &&
62
- error.details.reason_code === "mainline_not_at_base");
63
- }
64
56
  function runLineageFields(input) {
65
57
  return {
66
58
  run_lineage_kind: input.run_lineage.kind,
@@ -89,8 +81,8 @@ function logRunPipelineStage(options, input, fields, message) {
89
81
  }
90
82
  function stageProgressSummary(stage) {
91
83
  switch (stage) {
92
- case "run_workspace_prepare":
93
- return "preparing an isolated run workspace.";
84
+ case "workspace_inspect":
85
+ return "inspecting the persistent project workspace.";
94
86
  case "provider_task_run":
95
87
  return "running Codex task implementation.";
96
88
  case "provider_retry":
@@ -103,16 +95,10 @@ function stageProgressSummary(stage) {
103
95
  return "running project checks.";
104
96
  case "post_checks_diff_collect":
105
97
  return "rechecking candidate changes after checks.";
106
- case "result_commit":
107
- return "creating the candidate result commit.";
108
- case "promotion":
109
- return "promoting candidate changes to mainline.";
110
- case "promotion_reconcile":
111
- return "reconciling promotion after mainline changed.";
98
+ case "accepted_checkpoint":
99
+ return "saving the validated project state as an accepted checkpoint.";
112
100
  case "self_correction":
113
101
  return "retrying task implementation after pipeline feedback.";
114
- case "run_workspace_cleanup":
115
- return "cleaning up the run workspace.";
116
102
  default:
117
103
  return `running stage ${stage}.`;
118
104
  }
@@ -176,31 +162,6 @@ function loggedSyncStage(options, input, stage, run, completedFields = () => ({}
176
162
  throw error;
177
163
  }
178
164
  }
179
- function runResultLogFields(result) {
180
- if (result.result_kind === "finished") {
181
- return {
182
- result_kind: result.result_kind,
183
- outcome: result.outcome,
184
- checks: result.checks,
185
- docs: result.docs,
186
- changed_path_count: result.changed_paths?.length ?? 0,
187
- promotion_status: result.promotion?.status,
188
- };
189
- }
190
- if (result.result_kind === "failed") {
191
- return {
192
- result_kind: result.result_kind,
193
- error_code: result.error_code,
194
- retryable: result.retryable,
195
- changed_path_count: result.changed_paths?.length ?? 0,
196
- promotion_status: result.promotion?.status,
197
- };
198
- }
199
- return {
200
- result_kind: result.result_kind,
201
- changed_path_count: result.changed_paths?.length ?? 0,
202
- };
203
- }
204
165
  const MAX_SELF_CORRECTION_RETRIES = 1;
205
166
  const MAX_PROVIDER_TRANSIENT_RETRIES = 1;
206
167
  function runPipelineResultPromotion(result) {
@@ -264,18 +225,6 @@ function appendSelfCorrectionExhaustedSummary(input) {
264
225
  summary: `${input.result.summary} Self-correction retry limit reached after ${input.attemptedRetries} retry attempt${input.attemptedRetries === 1 ? "" : "s"}.`,
265
226
  };
266
227
  }
267
- function resultTimelineEvents(result) {
268
- return result.timeline_events ?? [];
269
- }
270
- function withCarriedTimelineEvents(result, carriedTimelineEvents) {
271
- if (carriedTimelineEvents.length === 0) {
272
- return result;
273
- }
274
- return {
275
- ...result,
276
- timeline_events: [...carriedTimelineEvents, ...resultTimelineEvents(result)],
277
- };
278
- }
279
228
  function isSelfCorrectionRequest(result) {
280
229
  return "request_kind" in result && result.request_kind === "self_correction";
281
230
  }
@@ -285,35 +234,12 @@ function isProviderTransientRunFailure(result) {
285
234
  result.retryable &&
286
235
  result.error_code.startsWith("codex_app_server_"));
287
236
  }
288
- function reconcileSelfCorrectionReason(result) {
289
- const promotion = runPipelineResultPromotion(result);
290
- if (promotion?.status !== "not_promoted") {
291
- return undefined;
292
- }
293
- if (promotion.reason_code !== undefined) {
294
- return promotion.reason_code;
295
- }
296
- if (result.result_kind === "failed") {
297
- return result.error_code;
298
- }
299
- if (result.result_kind === "finished" && result.outcome === "failed") {
300
- return "promotion_reconcile_failed";
301
- }
302
- return undefined;
303
- }
304
237
  async function runTaskAttempt(options, input) {
305
- let handle;
306
238
  let changedPaths;
307
239
  try {
308
- const runHandle = loggedSyncStage(options, input, "run_workspace_prepare", () => prepareRunWorkspace({
309
- workspaceRoot: options.workspaceRoot,
310
- runWorkspaceRoot: options.runWorkspaceRoot,
311
- taskId: input.task.id,
312
- activityRef: input.activity_ref,
313
- }));
314
- handle = runHandle;
315
- const taskOutput = await loggedAsyncStage(options, input, "provider_task_run", () => runWorkspaceWriteTask(options, input, runHandle), (output) => ({ decision: output.decision }));
316
- const diff = loggedSyncStage(options, input, "candidate_diff_collect", () => collectRunDiff(runHandle), (result) => ({ changed_path_count: result.changed_paths.length }));
240
+ loggedSyncStage(options, input, "workspace_inspect", () => inspectProjectWorkspace(options.workspaceRoot), (inspection) => ({ head_oid: inspection.head_ref }));
241
+ const taskOutput = await loggedAsyncStage(options, input, "provider_task_run", () => runWorkspaceWriteTask(options, input), (output) => ({ decision: output.decision }));
242
+ const diff = loggedSyncStage(options, input, "candidate_diff_collect", () => collectProjectSourceDiff(options.workspaceRoot), (result) => ({ changed_path_count: result.changed_paths.length }));
317
243
  changedPaths = diff.changed_paths;
318
244
  if (taskOutput.decision === "needs_human") {
319
245
  logRunPipelineStage(options, input, {
@@ -338,7 +264,7 @@ async function runTaskAttempt(options, input) {
338
264
  promotion: promotionNotAttempted(errorCode, taskOutput.summary),
339
265
  });
340
266
  }
341
- if (changedPaths.length === 0 && taskOutput.decision === "completed_no_repo_changes") {
267
+ if (!diff.has_changes && taskOutput.decision === "completed_no_repo_changes") {
342
268
  return completedWithoutRepoChanges({
343
269
  summary: taskOutput.summary,
344
270
  ...(taskOutput.user_note_candidate === undefined
@@ -346,7 +272,7 @@ async function runTaskAttempt(options, input) {
346
272
  : { userNoteCandidate: taskOutput.user_note_candidate }),
347
273
  });
348
274
  }
349
- if (changedPaths.length === 0) {
275
+ if (!diff.has_changes) {
350
276
  const result = failedBeforeChecks({
351
277
  summary: "Codex completed the Run but produced no candidate changes.",
352
278
  errorCode: "no_candidate_changes",
@@ -361,7 +287,7 @@ async function runTaskAttempt(options, input) {
361
287
  });
362
288
  }
363
289
  const artifactGate = await loggedAsyncStage(options, input, "artifact_candidate_validate", () => runArtifactCandidateGate({
364
- repoRoot: runHandle.repo_root,
290
+ repoRoot: options.workspaceRoot,
365
291
  task: {
366
292
  title: input.task.title,
367
293
  goal: input.task_detail.detail.contract.goal,
@@ -409,9 +335,9 @@ async function runTaskAttempt(options, input) {
409
335
  });
410
336
  }
411
337
  let docs = docsStatusForChangedPaths(changedPaths);
412
- const checkPlan = createNodeNpmCheckPlan({ workspaceRoot: runHandle.repo_root });
338
+ const checkPlan = createNodeNpmCheckPlan({ workspaceRoot: options.workspaceRoot });
413
339
  const checks = await loggedAsyncStage(options, input, "checks", () => executeCheckPlan({
414
- workspaceRoot: runHandle.repo_root,
340
+ workspaceRoot: options.workspaceRoot,
415
341
  plan: checkPlan,
416
342
  ...(options.checkTimeoutMs === undefined ? {} : { timeoutMs: options.checkTimeoutMs }),
417
343
  }), (result) => ({ checks_status: result.status, command_count: result.commands.length }));
@@ -429,7 +355,7 @@ async function runTaskAttempt(options, input) {
429
355
  guidance: "Use the failed checks summary to fix the candidate so the task can pass Tutti checks.",
430
356
  });
431
357
  }
432
- const postChecksDiff = loggedSyncStage(options, input, "post_checks_diff_collect", () => collectRunDiff(runHandle), (result) => ({ changed_path_count: result.changed_paths.length }));
358
+ const postChecksDiff = loggedSyncStage(options, input, "post_checks_diff_collect", () => collectProjectSourceDiff(options.workspaceRoot), (result) => ({ changed_path_count: result.changed_paths.length }));
433
359
  changedPaths = postChecksDiff.changed_paths;
434
360
  docs = docsStatusForChangedPaths(changedPaths);
435
361
  if (candidateChangedAfterChecks(diff, postChecksDiff)) {
@@ -449,53 +375,23 @@ async function runTaskAttempt(options, input) {
449
375
  guidance: "Prevent checks from mutating tracked files, or intentionally produce those generated files before checks run.",
450
376
  });
451
377
  }
452
- const commit = loggedSyncStage(options, input, "result_commit", () => createResultCommit(runHandle, `Complete Tutti task: ${input.task.title}`), (result) => ({ commit_oid: result.commit_oid }));
453
- let promotion;
454
- try {
455
- promotion = loggedSyncStage(options, input, "promotion", () => promoteRunResult(runHandle, commit), (result) => ({ promoted_commit_oid: result.promoted_commit_oid }));
456
- }
457
- catch (error) {
458
- if (isMainlineMovedPromotionConflict(error)) {
459
- const reconcileOutcome = await loggedAsyncStage(options, input, "promotion_reconcile", () => runPromotionReconcile({
460
- options,
461
- pipelineInput: input,
462
- runHandle,
463
- taskOutput,
464
- sourceCommit: commit,
465
- changedPaths: postChecksDiff.changed_paths,
466
- }), (outcome) => runResultLogFields(outcome.result));
467
- const reconcileResult = reconcileOutcome.result;
468
- if (reconcileOutcome.artifact_gate_failure !== undefined) {
469
- if (!reconcileOutcome.artifact_gate_failure.correctable) {
470
- return reconcileResult;
471
- }
472
- const artifactFailure = reconcileOutcome.artifact_gate_failure.failure;
473
- return requestSelfCorrection({
474
- result: reconcileResult,
475
- reasonCode: artifactFailure.reason_code,
476
- guidance: artifactFailure.guidance,
477
- diagnostic: artifactFailure.diagnostic,
478
- });
479
- }
480
- const reasonCode = reconcileSelfCorrectionReason(reconcileResult);
481
- if (reasonCode !== undefined) {
482
- return requestSelfCorrection({
483
- result: reconcileResult,
484
- reasonCode,
485
- guidance: "Rebuild the candidate from the latest mainline and resolve the promotion reconcile failure.",
486
- });
487
- }
488
- return reconcileResult;
489
- }
490
- throw error;
491
- }
378
+ const checkpoint = loggedSyncStage(options, input, "accepted_checkpoint", () => createAcceptedCheckpoint({
379
+ workspaceRoot: options.workspaceRoot,
380
+ taskId: input.task.id,
381
+ activityRef: input.activity_ref,
382
+ summary: `Complete Tutti task: ${input.task.title}`,
383
+ expectedSourceFingerprint: postChecksDiff.fingerprint,
384
+ }), (result) => ({
385
+ promoted_commit_oid: result.promoted_commit_oid,
386
+ checkpoint_disposition: result.disposition,
387
+ }));
492
388
  return {
493
389
  result_kind: "finished",
494
390
  outcome: "completed",
495
391
  summary: successfulRunSummary({
496
392
  implementation: taskOutput.summary,
497
393
  checks,
498
- completion: "Promotion complete.",
394
+ completion: "Accepted checkpoint complete.",
499
395
  }),
500
396
  ...(taskOutput.user_note_candidate === undefined
501
397
  ? {}
@@ -506,14 +402,25 @@ async function runTaskAttempt(options, input) {
506
402
  changed_paths: changedPaths,
507
403
  promotion: {
508
404
  status: "promoted",
509
- commit_oid: promotion.promoted_commit_oid,
510
- summary: "Promoted by fast-forward after checks.",
405
+ commit_oid: checkpoint.promoted_commit_oid,
406
+ summary: "Accepted checkpoint created after validation and checks.",
511
407
  },
512
408
  };
513
409
  }
514
410
  catch (error) {
411
+ if (changedPaths === undefined) {
412
+ try {
413
+ changedPaths = collectProjectSourceDiff(options.workspaceRoot).changed_paths;
414
+ }
415
+ catch {
416
+ // Preserve the original pipeline error when the best-effort diff cannot be collected.
417
+ }
418
+ }
515
419
  if (error instanceof CodexAppServerWorkspaceWriteRunError) {
516
- return summarizeProviderError(error);
420
+ return {
421
+ ...summarizeProviderError(error),
422
+ ...(changedPaths === undefined ? {} : { changed_paths: changedPaths }),
423
+ };
517
424
  }
518
425
  if (error instanceof WorkspaceOpsError) {
519
426
  return summarizeWorkspaceOpsError(error, changedPaths);
@@ -525,12 +432,6 @@ async function runTaskAttempt(options, input) {
525
432
  ...(changedPaths === undefined ? {} : { changedPaths }),
526
433
  });
527
434
  }
528
- finally {
529
- const cleanupHandle = handle;
530
- if (cleanupHandle !== undefined) {
531
- loggedSyncStage(options, input, "run_workspace_cleanup", () => cleanupRunWorkspace(cleanupHandle));
532
- }
533
- }
534
435
  }
535
436
  export function createOpenAiRunPipelineRunner(options) {
536
437
  return {
@@ -538,7 +439,6 @@ export function createOpenAiRunPipelineRunner(options) {
538
439
  let currentInput = input;
539
440
  let selfCorrectionRetries = 0;
540
441
  let providerTransientRetries = 0;
541
- let carriedTimelineEvents = [];
542
442
  for (;;) {
543
443
  const result = await runTaskAttempt(options, currentInput);
544
444
  if (isProviderTransientRunFailure(result) &&
@@ -558,18 +458,14 @@ export function createOpenAiRunPipelineRunner(options) {
558
458
  }
559
459
  providerTransientRetries = 0;
560
460
  if (!isSelfCorrectionRequest(result)) {
561
- return withCarriedTimelineEvents(result, carriedTimelineEvents);
461
+ return result;
562
462
  }
563
463
  if (selfCorrectionRetries >= MAX_SELF_CORRECTION_RETRIES) {
564
- return withCarriedTimelineEvents(appendSelfCorrectionExhaustedSummary({
464
+ return appendSelfCorrectionExhaustedSummary({
565
465
  result: result.fallback,
566
466
  attemptedRetries: selfCorrectionRetries,
567
- }), carriedTimelineEvents);
467
+ });
568
468
  }
569
- carriedTimelineEvents = [
570
- ...carriedTimelineEvents,
571
- ...resultTimelineEvents(result.fallback),
572
- ];
573
469
  selfCorrectionRetries += 1;
574
470
  const correctionContext = result.correction_context;
575
471
  const nextAttempt = currentInput.run_lineage.attempt + 1;
@@ -609,8 +505,6 @@ export function resolveProjectOpenAiRunPipelineRunner(options) {
609
505
  config,
610
506
  apiKey: credential.secret,
611
507
  workspaceRoot: options.workspaceRoot,
612
- runWorkspaceRoot: join(options.runWorkspaceRoot, "run-workspaces"),
613
- mergeWorkspaceRoot: join(options.mergeWorkspaceRoot ?? options.runWorkspaceRoot, "merge-workspaces"),
614
508
  tuttiHome: options.tuttiHome,
615
509
  userSkillsRoot: getProjectUserSkillsRoot(options.tuttiHome, options.projectId),
616
510
  ...(options.agentContext === undefined ? {} : { agentContext: options.agentContext }),
@@ -1,6 +1,5 @@
1
1
  import type { RunPipelineInput } from "../control-plane/index.js";
2
- import type { RunWorkspaceHandle } from "../workspace-ops/index.js";
3
2
  import type { OpenAiRunPipelineRunnerOptions } from "./openai.js";
4
3
  import { type TaskRunOutput } from "./task-run-output.js";
5
- export declare function runWorkspaceWriteTask(options: OpenAiRunPipelineRunnerOptions, input: RunPipelineInput, handle: RunWorkspaceHandle): Promise<TaskRunOutput>;
4
+ export declare function runWorkspaceWriteTask(options: OpenAiRunPipelineRunnerOptions, input: RunPipelineInput): Promise<TaskRunOutput>;
6
5
  //# sourceMappingURL=task-run-invocation.d.ts.map
@@ -56,7 +56,7 @@ function usageSource(input) {
56
56
  return "workspace_write_run";
57
57
  }
58
58
  }
59
- export async function runWorkspaceWriteTask(options, input, handle) {
59
+ export async function runWorkspaceWriteTask(options, input) {
60
60
  const manager = options.approvalManager;
61
61
  const approvalHandler = manager === undefined
62
62
  ? undefined
@@ -66,7 +66,6 @@ export async function runWorkspaceWriteTask(options, input, handle) {
66
66
  taskId: input.task.id,
67
67
  activityRef: input.activity_ref,
68
68
  workspaceRoot: approvalInput.context.workspaceRoot,
69
- runWorkspaceRoot: approvalInput.context.runWorkspaceRoot,
70
69
  codexHome: approvalInput.context.codexHome,
71
70
  ...(approvalInput.context.tuttiHome === undefined
72
71
  ? {}
@@ -82,7 +81,7 @@ export async function runWorkspaceWriteTask(options, input, handle) {
82
81
  try {
83
82
  result = await runCodexAppServerWorkspaceWriteRun({
84
83
  apiKey: options.apiKey,
85
- workspaceRoot: handle.repo_root,
84
+ workspaceRoot: options.workspaceRoot,
86
85
  prompt: createRunPrompt(options, input),
87
86
  outputSchema: TASK_RUN_OUTPUT_SCHEMA,
88
87
  validateOutput: (value) => isInitialTaskRunOutput(value),
@@ -90,7 +89,6 @@ export async function runWorkspaceWriteTask(options, input, handle) {
90
89
  defaultModel: options.config.default_model,
91
90
  approvalContext: {
92
91
  workspaceRoot: options.workspaceRoot,
93
- runWorkspaceRoot: handle.repo_root,
94
92
  taskId: input.task.id,
95
93
  activityRef: input.activity_ref,
96
94
  },
@@ -4,7 +4,6 @@ import { completeRelayUpload, resolveRelayUpload } from "@tutti/relay-client";
4
4
  import { ApprovalRequestManager } from "../../approvals/index.js";
5
5
  import { ReadOnlyChatAssistant } from "../../chat-assistant/index.js";
6
6
  import { ensureInitialProjectWelcomeMessage } from "../../collaboration-state/index.js";
7
- import { resolveControlPlaneCommandRecovery } from "../../control-plane/command-recovery.js";
8
7
  import { Phase5ControlPlane } from "../../control-plane/index.js";
9
8
  import { resolveProjectOpenAiProcedureWorkflowRunner } from "../../control-plane/workflows/index.js";
10
9
  import { resolveProjectOpenAiReadOnlyChatAssistantModel } from "../../providers/openai/chat-assistant.js";
@@ -12,7 +11,6 @@ import { configureProjectOpenAiProvider, readOpenAiProviderConfigProjection, } f
12
11
  import { readProviderUsageProjection, recordProviderUsageEvent, } from "../../provider-usage/index.js";
13
12
  import { resolveProjectOpenAiRunPipelineRunner } from "../../run-pipeline/openai.js";
14
13
  import { openHostProjectStore } from "../../store/index.js";
15
- import { cleanupStaleRunWorkspaces } from "../../workspace-ops/index.js";
16
14
  import { resolvePackagedWebStaticRoot } from "../http/static-web.js";
17
15
  import { AGENT_CONTEXT_BASE_PATH, AgentContextTokenRegistry, } from "../http/routes/agent-context.js";
18
16
  import { readHostRepoProjection } from "../http/routes/project-api.js";
@@ -133,7 +131,7 @@ export async function startForegroundHostServer(options) {
133
131
  const store = openHostProjectStore({
134
132
  localStoreRoot,
135
133
  projectId: options.preparation.project_id,
136
- commandRecoveryResolver: resolveControlPlaneCommandRecovery,
134
+ deferCommandRecovery: true,
137
135
  now,
138
136
  });
139
137
  const workspaceEvents = new WorkspaceEventBus({ now });
@@ -224,7 +222,6 @@ export async function startForegroundHostServer(options) {
224
222
  tuttiHome: options.preparation.tutti_home,
225
223
  projectId: options.preparation.project_id,
226
224
  workspaceRoot: options.preparation.workspace_root,
227
- runWorkspaceRoot: localStoreRoot,
228
225
  agentContext: agentContextRuntime,
229
226
  approvalManager,
230
227
  logger: runPipelineLogger,
@@ -428,12 +425,8 @@ export async function startForegroundHostServer(options) {
428
425
  throw error;
429
426
  }
430
427
  controlPlane.runStartupRecovery({
431
- interruptedCommandCount: store.interrupted_command_count,
432
- cleanupStaleRunWorkspaces: () => cleanupStaleRunWorkspaces({
433
- workspaceRoot: options.preparation.workspace_root,
434
- runWorkspaceRoot: join(localStoreRoot, "run-workspaces"),
435
- mergeWorkspaceRoot: join(localStoreRoot, "merge-workspaces"),
436
- }),
428
+ workspaceRoot: options.preparation.workspace_root,
429
+ recoverInProgressCommands: true,
437
430
  });
438
431
  let resolveClosed;
439
432
  const closed = new Promise((resolve) => {
@@ -17,6 +17,7 @@ export type OpenHostProjectStoreOptions = {
17
17
  projectId: ProjectId;
18
18
  migrationsDirectory?: string;
19
19
  commandRecoveryResolver?: CommandRecoveryResolver;
20
+ deferCommandRecovery?: boolean;
20
21
  now?: () => Date;
21
22
  };
22
23
  export declare function openHostProjectStore(options: OpenHostProjectStoreOptions): HostProjectStore;
@@ -22,12 +22,14 @@ export function openHostProjectStore(options) {
22
22
  projectId: options.projectId,
23
23
  now,
24
24
  });
25
- const commandRecovery = recoverInProgressCommandIdempotencyRecords(db, {
26
- ...(options.commandRecoveryResolver === undefined
27
- ? {}
28
- : { resolve: options.commandRecoveryResolver }),
29
- now,
30
- });
25
+ const commandRecovery = options.deferCommandRecovery
26
+ ? { total_count: 0, completed_count: 0, failed_count: 0, interrupted_count: 0 }
27
+ : recoverInProgressCommandIdempotencyRecords(db, {
28
+ ...(options.commandRecoveryResolver === undefined
29
+ ? {}
30
+ : { resolve: options.commandRecoveryResolver }),
31
+ now,
32
+ });
31
33
  return {
32
34
  db,
33
35
  database_path: databasePath,
@@ -1,11 +1,12 @@
1
1
  export { DEFAULT_VIEWER_MAX_BYTES, MAX_REFERENCE_UPLOAD_BYTES, MAX_VIEWER_MAX_BYTES, PROJECT_DOC_PATHS, REFERENCE_IMAGE_ASSET_CACHE_VERSION, REFERENCE_IMAGE_PREVIEW_MAX_EDGE_PX, REFERENCE_IMAGE_THUMBNAIL_MAX_EDGE_PX, REFERENCE_DIRECTORY_PATH, REFERENCE_FILES_DIRECTORY_PATH, REFERENCE_IMAGES_DIRECTORY_PATH, REFERENCE_INDEX_PATH, REFERENCE_TUTTI_DIRECTORY_PATH, VIEWER_ASSET_READ_LIMIT_BYTES, VIEWER_HARD_READ_LIMIT_BYTES, WORKSPACE_OPS_MAINLINE_BRANCH, } from "./constants.js";
2
2
  export { WorkspaceOpsError } from "./errors.js";
3
+ export { commitOwnedPaths, requireOwnedPathsWriteReady } from "./system-commits.js";
4
+ export { collectProjectSourceDiff, createAcceptedCheckpoint, fingerprintProjectSourceState, inspectProjectWorkspace, recoverAcceptedCheckpoint, } from "./project-workspace.js";
3
5
  export { initializeProjectDocs, readProjectDocsInitializationStatus, syncProjectDocs, } from "./project-docs.js";
4
6
  export { uploadReferenceFile } from "./reference-files.js";
5
7
  export { referenceFileCategoryForPath, referenceFileSourceForPath, referenceMediaTypeForPath, referenceTuttiGroupForPath, } from "./reference-metadata.js";
6
8
  export { readReferenceSummaryIndex, readReferenceSummaryTargets, referenceSummaryProjectionForEntry, updateReferenceSummaries, } from "./reference-summaries.js";
7
- export { applyRunResultToMergeWorkspace, cleanupMergeWorkspace, cleanupRunWorkspace, cleanupStaleRunWorkspaces, collectMergeDiff, collectRunDiff, createMergeCandidateCommit, createResultCommit, fingerprintWorkspaceCandidate, prepareMergeWorkspace, prepareRunWorkspace, promoteMergeCandidate, promoteRunResult, } from "./run-workspaces.js";
8
9
  export { readViewerAsset, readViewerFile, readViewerSnapshot, readViewerTree } from "./viewer.js";
9
10
  export type { WorkspaceOpsErrorCode } from "./errors.js";
10
- export type { CleanupRunWorkspaceResult, CleanupStaleRunWorkspacesResult, InitializeProjectDocsOptions, InitializeProjectDocsResult, MergeWorkspaceHandle, PrepareMergeWorkspaceOptions, PrepareRunWorkspaceOptions, ProjectDocsInitializationContext, ProjectDocsInitializationDocument, ProjectDocsInitializationStatus, PromotionResult, ReadViewerAssetOptions, ReadViewerFileOptions, ReadViewerTreeOptions, ReferenceFileCategory, ReferenceSummaryProjection, ReferenceSummaryRecord, ReferenceSummaryStatus, ReferenceSummaryTarget, ResultCommit, RunDiffSummary, RunWorkspaceHandle, SyncProjectDocsResult, SyncProjectDocsUpdate, UpdateReferenceSummariesOptions, UpdateReferenceSummariesResult, UploadReferenceFileOptions, UploadReferenceFileResult, ViewerAssetResult, ViewerAssetVariant, ViewerEntryKind, ViewerFileContent, ViewerFileResult, ViewerRepoSnapshot, ViewerTreeEntry, ViewerTreeResult, WorkspaceViewerOptions, } from "./types.js";
11
+ export type { AcceptedCheckpointRecoveryResult, AcceptedCheckpointResult, CreateAcceptedCheckpointOptions, InitializeProjectDocsOptions, InitializeProjectDocsResult, ProjectDocsInitializationContext, ProjectDocsInitializationDocument, ProjectDocsInitializationStatus, ProjectWorkspaceInspection, ReadViewerAssetOptions, ReadViewerFileOptions, ReadViewerTreeOptions, ReferenceFileCategory, ReferenceSummaryProjection, ReferenceSummaryRecord, ReferenceSummaryStatus, ReferenceSummaryTarget, RunDiffSummary, SyncProjectDocsResult, SyncProjectDocsUpdate, UpdateReferenceSummariesOptions, UpdateReferenceSummariesResult, UploadReferenceFileOptions, UploadReferenceFileResult, ViewerAssetResult, ViewerAssetVariant, ViewerEntryKind, ViewerFileContent, ViewerFileResult, ViewerRepoSnapshot, ViewerTreeEntry, ViewerTreeResult, WorkspaceViewerOptions, } from "./types.js";
11
12
  //# sourceMappingURL=index.d.ts.map
@@ -1,9 +1,10 @@
1
1
  export { DEFAULT_VIEWER_MAX_BYTES, MAX_REFERENCE_UPLOAD_BYTES, MAX_VIEWER_MAX_BYTES, PROJECT_DOC_PATHS, REFERENCE_IMAGE_ASSET_CACHE_VERSION, REFERENCE_IMAGE_PREVIEW_MAX_EDGE_PX, REFERENCE_IMAGE_THUMBNAIL_MAX_EDGE_PX, REFERENCE_DIRECTORY_PATH, REFERENCE_FILES_DIRECTORY_PATH, REFERENCE_IMAGES_DIRECTORY_PATH, REFERENCE_INDEX_PATH, REFERENCE_TUTTI_DIRECTORY_PATH, VIEWER_ASSET_READ_LIMIT_BYTES, VIEWER_HARD_READ_LIMIT_BYTES, WORKSPACE_OPS_MAINLINE_BRANCH, } from "./constants.js";
2
2
  export { WorkspaceOpsError } from "./errors.js";
3
+ export { commitOwnedPaths, requireOwnedPathsWriteReady } from "./system-commits.js";
4
+ export { collectProjectSourceDiff, createAcceptedCheckpoint, fingerprintProjectSourceState, inspectProjectWorkspace, recoverAcceptedCheckpoint, } from "./project-workspace.js";
3
5
  export { initializeProjectDocs, readProjectDocsInitializationStatus, syncProjectDocs, } from "./project-docs.js";
4
6
  export { uploadReferenceFile } from "./reference-files.js";
5
7
  export { referenceFileCategoryForPath, referenceFileSourceForPath, referenceMediaTypeForPath, referenceTuttiGroupForPath, } from "./reference-metadata.js";
6
8
  export { readReferenceSummaryIndex, readReferenceSummaryTargets, referenceSummaryProjectionForEntry, updateReferenceSummaries, } from "./reference-summaries.js";
7
- export { applyRunResultToMergeWorkspace, cleanupMergeWorkspace, cleanupRunWorkspace, cleanupStaleRunWorkspaces, collectMergeDiff, collectRunDiff, createMergeCandidateCommit, createResultCommit, fingerprintWorkspaceCandidate, prepareMergeWorkspace, prepareRunWorkspace, promoteMergeCandidate, promoteRunResult, } from "./run-workspaces.js";
8
9
  export { readViewerAsset, readViewerFile, readViewerSnapshot, readViewerTree } from "./viewer.js";
9
10
  //# sourceMappingURL=index.js.map
@@ -1,2 +1,2 @@
1
- export declare function requireMainlineWriteReady(workspaceRoot: string): void;
1
+ export declare function requireMainlineBranch(workspaceRoot: string): void;
2
2
  //# sourceMappingURL=mainline.d.ts.map
@@ -2,15 +2,11 @@ import { WORKSPACE_OPS_MAINLINE_BRANCH } from "./constants.js";
2
2
  import { WorkspaceOpsError } from "./errors.js";
3
3
  import { tryGitText } from "./git.js";
4
4
  import { readViewerSnapshot } from "./viewer.js";
5
- export function requireMainlineWriteReady(workspaceRoot) {
5
+ export function requireMainlineBranch(workspaceRoot) {
6
6
  readViewerSnapshot({ workspaceRoot });
7
7
  const currentBranch = tryGitText(["branch", "--show-current"], workspaceRoot);
8
8
  if (currentBranch !== WORKSPACE_OPS_MAINLINE_BRANCH) {
9
9
  throw new WorkspaceOpsError("conflict", "Mainline write requires the mainline branch to be checked out");
10
10
  }
11
- const status = tryGitText(["status", "--porcelain"], workspaceRoot);
12
- if (status !== "") {
13
- throw new WorkspaceOpsError("conflict", "Mainline write requires a clean mainline working tree");
14
- }
15
11
  }
16
12
  //# sourceMappingURL=mainline.js.map
@@ -2,8 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { extname, join, posix } from "node:path";
3
3
  import { PROJECT_DOC_PATHS } from "./constants.js";
4
4
  import { WorkspaceOpsError } from "./errors.js";
5
- import { runGit, runGitText, stagedChangesExist } from "./git.js";
6
- import { requireMainlineWriteReady } from "./mainline.js";
5
+ import { commitOwnedPaths, requireOwnedPathsWriteReady } from "./system-commits.js";
7
6
  import { projectDocTemplate, repoFactHints } from "./project-doc-templates.js";
8
7
  import { entryKind, lsPath, readViewerSnapshot, requireNormalizedViewerPath, } from "./viewer.js";
9
8
  const MAX_PROJECT_DOC_SYNC_BYTES = 512 * 1024;
@@ -61,7 +60,6 @@ export function readProjectDocsInitializationStatus(options) {
61
60
  };
62
61
  }
63
62
  export function initializeProjectDocs(options) {
64
- requireMainlineWriteReady(options.workspaceRoot);
65
63
  const createdPaths = [];
66
64
  const repoHints = repoFactHints(options.workspaceRoot);
67
65
  const providedDocuments = new Map();
@@ -81,6 +79,11 @@ export function initializeProjectDocs(options) {
81
79
  providedDocuments.set(path, document.content);
82
80
  }
83
81
  }
82
+ const missingPaths = PROJECT_DOC_PATHS.filter((path) => !projectDocExists(options.workspaceRoot, path));
83
+ if (missingPaths.length === 0) {
84
+ return { created_paths: [] };
85
+ }
86
+ requireOwnedPathsWriteReady(options.workspaceRoot, missingPaths);
84
87
  for (const path of PROJECT_DOC_PATHS) {
85
88
  if (projectDocExists(options.workspaceRoot, path)) {
86
89
  continue;
@@ -89,20 +92,20 @@ export function initializeProjectDocs(options) {
89
92
  writeFileSync(join(options.workspaceRoot, path), `${content.trimEnd()}\n`, "utf8");
90
93
  createdPaths.push(path);
91
94
  }
92
- if (createdPaths.length === 0) {
93
- return { created_paths: [] };
94
- }
95
- runGit(["add", "--", ...createdPaths], options.workspaceRoot);
96
- if (stagedChangesExist(options.workspaceRoot)) {
97
- runGit(["commit", "-m", "Initialize project docs"], options.workspaceRoot);
95
+ const commitOid = commitOwnedPaths({
96
+ workspaceRoot: options.workspaceRoot,
97
+ paths: createdPaths,
98
+ message: "Initialize project docs",
99
+ });
100
+ if (commitOid === null) {
101
+ throw new WorkspaceOpsError("git_failed", "Project docs write produced no owned-path commit");
98
102
  }
99
103
  return {
100
104
  created_paths: createdPaths,
101
- commit_oid: runGitText(["rev-parse", "--verify", "HEAD"], options.workspaceRoot),
105
+ commit_oid: commitOid,
102
106
  };
103
107
  }
104
108
  export function syncProjectDocs(options) {
105
- requireMainlineWriteReady(options.workspaceRoot);
106
109
  const normalizedUpdates = options.updates.map((update) => {
107
110
  const normalizedPath = requireProjectDocsSyncPath({
108
111
  workspaceRoot: options.workspaceRoot,
@@ -127,6 +130,10 @@ export function syncProjectDocs(options) {
127
130
  }
128
131
  seenPaths.add(update.path);
129
132
  }
133
+ if (normalizedUpdates.length === 0) {
134
+ return { updated_paths: [] };
135
+ }
136
+ requireOwnedPathsWriteReady(options.workspaceRoot, normalizedUpdates.map((update) => update.path));
130
137
  const updatedPaths = [];
131
138
  for (const update of normalizedUpdates) {
132
139
  const filesystemPath = join(options.workspaceRoot, ...update.path.split("/"));
@@ -144,13 +151,17 @@ export function syncProjectDocs(options) {
144
151
  if (updatedPaths.length === 0) {
145
152
  return { updated_paths: [] };
146
153
  }
147
- runGit(["add", "--", ...updatedPaths], options.workspaceRoot);
148
- if (stagedChangesExist(options.workspaceRoot)) {
149
- runGit(["commit", "-m", "Sync project docs"], options.workspaceRoot);
154
+ const commitOid = commitOwnedPaths({
155
+ workspaceRoot: options.workspaceRoot,
156
+ paths: updatedPaths,
157
+ message: "Sync project docs",
158
+ });
159
+ if (commitOid === null) {
160
+ throw new WorkspaceOpsError("git_failed", "Project docs sync produced no owned-path commit");
150
161
  }
151
162
  return {
152
163
  updated_paths: updatedPaths,
153
- commit_oid: runGitText(["rev-parse", "--verify", "HEAD"], options.workspaceRoot),
164
+ commit_oid: commitOid,
154
165
  };
155
166
  }
156
167
  //# sourceMappingURL=project-docs.js.map
@@ -0,0 +1,12 @@
1
+ import type { ActivityRef, TaskId } from "@tutti/shared/ids";
2
+ import type { AcceptedCheckpointRecoveryResult, AcceptedCheckpointResult, CreateAcceptedCheckpointOptions, ProjectWorkspaceInspection, RunDiffSummary } from "./types.js";
3
+ export declare function fingerprintProjectSourceState(workspaceRoot: string): string;
4
+ export declare function collectProjectSourceDiff(workspaceRoot: string): RunDiffSummary;
5
+ export declare function inspectProjectWorkspace(workspaceRoot: string): ProjectWorkspaceInspection;
6
+ export declare function recoverAcceptedCheckpoint(options: {
7
+ workspaceRoot: string;
8
+ taskId: TaskId;
9
+ activityRef: ActivityRef;
10
+ }): AcceptedCheckpointRecoveryResult;
11
+ export declare function createAcceptedCheckpoint(options: CreateAcceptedCheckpointOptions): AcceptedCheckpointResult;
12
+ //# sourceMappingURL=project-workspace.d.ts.map