@xfey/tutti 0.1.16 → 0.1.18

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 (57) hide show
  1. package/dist/collaboration-state/run-recording.js +3 -0
  2. package/dist/collaboration-state/run-results.js +3 -0
  3. package/dist/collaboration-state/types.d.ts +1 -0
  4. package/dist/control-plane/index.js +3 -0
  5. package/dist/control-plane/task-compile-start.d.ts +3 -0
  6. package/dist/control-plane/task-compile-start.js +47 -11
  7. package/dist/control-plane/types.d.ts +1 -0
  8. package/dist/control-plane/worklist-terminal-feedback.d.ts +1 -0
  9. package/dist/control-plane/worklist-terminal-feedback.js +109 -0
  10. package/dist/run-pipeline/openai.js +6 -0
  11. package/dist/run-pipeline/promotion-reconcile.d.ts +2 -2
  12. package/dist/run-pipeline/promotion-reconcile.js +3 -0
  13. package/dist/run-pipeline/result-projections.d.ts +1 -0
  14. package/dist/run-pipeline/result-projections.js +3 -0
  15. package/dist/run-pipeline/task-run-output.d.ts +9 -3
  16. package/dist/run-pipeline/task-run-output.js +24 -4
  17. package/dist/server-shell/cli/cli.js +3 -22
  18. package/dist/server-shell/cli/machine-local.d.ts +4 -0
  19. package/dist/server-shell/cli/machine-local.js +15 -1
  20. package/dist/server-shell/cli/project-resolver.d.ts +8 -0
  21. package/dist/server-shell/cli/project-resolver.js +67 -5
  22. package/dist/server-shell/cli/provider-tui.d.ts +12 -0
  23. package/dist/server-shell/cli/provider-tui.js +15 -20
  24. package/dist/server-shell/cli/runtime-commands.d.ts +10 -1
  25. package/dist/server-shell/cli/runtime-commands.js +45 -7
  26. package/dist/server-shell/http/routes/project-api/openapi-execution-routes.d.ts +1 -0
  27. package/dist/server-shell/http/routes/project-api/openapi-viewer-routes.d.ts +2 -0
  28. package/dist/server-shell/http/routes/project-api/openapi.d.ts +1 -0
  29. package/dist/server-shell/http/routes/project-api/reference-files.js +9 -3
  30. package/dist/workspace-ops/constants.d.ts +1 -0
  31. package/dist/workspace-ops/constants.js +1 -0
  32. package/dist/workspace-ops/index.d.ts +2 -2
  33. package/dist/workspace-ops/index.js +2 -2
  34. package/dist/workspace-ops/reference-metadata.d.ts +2 -0
  35. package/dist/workspace-ops/reference-metadata.js +26 -1
  36. package/node_modules/@tutti/shared/dist/schemas/api/clarifications.d.ts +9 -0
  37. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
  38. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
  39. package/node_modules/@tutti/shared/dist/schemas/api/messages.d.ts +52 -0
  40. package/node_modules/@tutti/shared/dist/schemas/api/messages.js +10 -0
  41. package/node_modules/@tutti/shared/dist/schemas/api/runtime-events.d.ts +1 -0
  42. package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +2 -1
  43. package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.d.ts +14 -0
  44. package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.js +2 -0
  45. package/node_modules/@tutti/shared/dist/schemas/api/work-items.d.ts +3 -0
  46. package/node_modules/@tutti/shared/dist/schemas/api/work-items.js +2 -0
  47. package/package.json +1 -1
  48. package/prompts/prompt-flow-map.md +2 -2
  49. package/prompts/runs/README.md +2 -1
  50. package/prompts/runs/task-continuation.md +8 -2
  51. package/prompts/runs/task-retry.md +8 -2
  52. package/prompts/runs/task-run.md +8 -2
  53. package/web/assets/index-4JInvPEN.js +29 -0
  54. package/web/assets/index-P5uZBm0s.css +1 -0
  55. package/web/index.html +2 -2
  56. package/web/assets/index-DoZOFmIq.js +0 -29
  57. package/web/assets/index-mXkNKFjm.css +0 -1
@@ -29,6 +29,9 @@ export function recordRunResult(db, input) {
29
29
  };
30
30
  if (input.result.result_kind === "finished") {
31
31
  detailResult.outcome = input.result.outcome;
32
+ if (input.result.user_note_candidate !== undefined) {
33
+ detailResult.user_note_candidate = input.result.user_note_candidate;
34
+ }
32
35
  detailResult.checks = input.result.checks;
33
36
  detailResult.docs = input.result.docs;
34
37
  if (input.result.skip_reason_code !== undefined) {
@@ -38,6 +38,9 @@ export function mapRunResultRow(row) {
38
38
  result_kind: "finished",
39
39
  outcome: result.outcome === "failed" ? "failed" : "completed",
40
40
  summary: typeof result.summary === "string" ? result.summary : "",
41
+ ...(typeof result.user_note_candidate === "string"
42
+ ? { user_note_candidate: result.user_note_candidate }
43
+ : {}),
41
44
  checks: result.checks === "failed" || result.checks === "skipped" ? result.checks : "passed",
42
45
  docs: result.docs === "checked_no_change" ||
43
46
  result.docs === "not_checked" ||
@@ -120,6 +120,7 @@ export type RecordRunResultInput = {
120
120
  summary: string;
121
121
  checks: "passed" | "failed" | "skipped";
122
122
  docs: "updated" | "checked_no_change" | "not_checked" | "sync_deferred";
123
+ user_note_candidate?: string;
123
124
  skip_reason_code?: TaskDetailResultProjection["skip_reason_code"];
124
125
  skip_reason_summary?: string;
125
126
  external_side_effects?: TaskDetailResultProjection["external_side_effects"];
@@ -313,6 +313,9 @@ export class Phase5ControlPlane {
313
313
  emitWorklistCompletedFeedback({
314
314
  store: this.store,
315
315
  events: this.events,
316
+ ...(this.projectContext === undefined
317
+ ? {}
318
+ : { workspaceRoot: this.projectContext.workspaceRoot }),
316
319
  now: this.now,
317
320
  });
318
321
  this.maybeStartContextSyncAfterWorklistCompletion();
@@ -40,5 +40,8 @@ export declare function runControlPlaneTaskCompileProcedure(options: {
40
40
  kind: "workflow";
41
41
  workflow_ref: `wf_${string}`;
42
42
  };
43
+ } | {
44
+ kind: "failed";
45
+ summary: string;
43
46
  }>;
44
47
  //# sourceMappingURL=task-compile-start.d.ts.map
@@ -51,7 +51,7 @@ export function startControlPlaneTaskCompile(options) {
51
51
  sourceBatch,
52
52
  });
53
53
  }
54
- if (scratchpad.state !== "ready") {
54
+ if (!scratchpadHasSubmittableContent(scratchpad)) {
55
55
  return { disposition: { kind: "scratchpad_empty" } };
56
56
  }
57
57
  if (shouldInitializeProjectContext) {
@@ -66,6 +66,10 @@ export function startControlPlaneTaskCompile(options) {
66
66
  });
67
67
  }
68
68
  function startControlPlaneTaskCompileActivity(options) {
69
+ const scratchpad = readScratchpadProjection(options.store.db);
70
+ if (!scratchpadHasSubmittableContent(scratchpad)) {
71
+ return { disposition: { kind: "scratchpad_empty" } };
72
+ }
69
73
  const workflowRef = createWorkflowInvocationRef();
70
74
  const start = options.engine.startProcedure({
71
75
  workflow_ref: workflowRef,
@@ -120,13 +124,18 @@ function startScratchpadRefreshBeforeTaskCompileActivity(options) {
120
124
  now: options.now,
121
125
  onTransition: (state) => {
122
126
  options.publishProcedureTransition(state);
123
- if (state.activity.status === "finished") {
124
- if (shouldInitializeControlPlaneProjectContext(options)) {
125
- void startProjectContextBootstrapActivity(options);
126
- }
127
- else {
128
- void startControlPlaneTaskCompileActivity(options);
129
- }
127
+ if (state.transition_kind !== "terminal" || state.activity.status !== "finished") {
128
+ return;
129
+ }
130
+ const scratchpad = readScratchpadProjection(options.store.db);
131
+ if (!scratchpadHasSubmittableContent(scratchpad)) {
132
+ return;
133
+ }
134
+ if (shouldInitializeControlPlaneProjectContext(options)) {
135
+ void startProjectContextBootstrapActivity(options);
136
+ }
137
+ else {
138
+ void startControlPlaneTaskCompileActivity(options);
130
139
  }
131
140
  },
132
141
  onError: ({ context, error }) => logProcedureExecutionError({
@@ -186,9 +195,17 @@ function startProjectContextBootstrapActivity(options) {
186
195
  now: options.now,
187
196
  onTransition: (state) => {
188
197
  options.publishProcedureTransition(state);
189
- if (state.activity.status === "finished") {
190
- void startControlPlaneTaskCompileActivity(options);
198
+ if (state.transition_kind !== "terminal" || state.activity.status !== "finished") {
199
+ return;
200
+ }
201
+ if (shouldInitializeControlPlaneProjectContext(options)) {
202
+ return;
191
203
  }
204
+ const scratchpad = readScratchpadProjection(options.store.db);
205
+ if (!scratchpadHasSubmittableContent(scratchpad)) {
206
+ return;
207
+ }
208
+ void startControlPlaneTaskCompileActivity(options);
192
209
  },
193
210
  onError: ({ context, error }) => logProcedureExecutionError({
194
211
  logger: options.logger,
@@ -261,9 +278,16 @@ function startProjectContextBootstrapActivity(options) {
261
278
  };
262
279
  }
263
280
  export async function runControlPlaneTaskCompileProcedure(options) {
281
+ const scratchpad = readScratchpadProjection(options.store.db);
282
+ if (!scratchpadHasSubmittableContent(scratchpad)) {
283
+ return {
284
+ kind: "failed",
285
+ summary: "Scratchpad is empty. Discuss more before running.",
286
+ };
287
+ }
264
288
  const output = await options.runner.runTaskCompile({
265
289
  workflow_ref: options.workflowRef,
266
- scratchpad: readScratchpadProjection(options.store.db),
290
+ scratchpad,
267
291
  worklist: readWorklistProjection(options.store.db),
268
292
  }, {
269
293
  workflow_ref: options.workflowRef,
@@ -282,4 +306,16 @@ export async function runControlPlaneTaskCompileProcedure(options) {
282
306
  onTasksAdded: options.onTasksAdded,
283
307
  });
284
308
  }
309
+ function scratchpadHasSubmittableContent(scratchpad) {
310
+ if (scratchpad.state !== "ready") {
311
+ return false;
312
+ }
313
+ return [
314
+ scratchpad.topic,
315
+ scratchpad.background_summary,
316
+ ...scratchpad.current_consensus,
317
+ ...scratchpad.open_questions,
318
+ ...scratchpad.task_changes,
319
+ ].some((item) => item.trim().length > 0);
320
+ }
285
321
  //# sourceMappingURL=task-compile-start.js.map
@@ -123,6 +123,7 @@ export type RunPipelineResult = RunPipelineResultMetadata & ({
123
123
  result_kind: "finished";
124
124
  outcome: "completed" | "failed";
125
125
  summary: string;
126
+ user_note_candidate?: string;
126
127
  checks: "passed" | "failed" | "skipped";
127
128
  docs: "updated" | "checked_no_change" | "not_checked" | "sync_deferred";
128
129
  skip_reason_code?: NonNullable<TaskDetailProjection["detail"]["result"]["skip_reason_code"]>;
@@ -5,6 +5,7 @@ import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js
5
5
  type EmitWorklistFeedbackInput = {
6
6
  store: HostProjectStore;
7
7
  events: WorkspaceEventBus;
8
+ workspaceRoot?: string;
8
9
  now: () => Date;
9
10
  };
10
11
  export declare function emitWorklistCompletedFeedback(input: EmitWorklistFeedbackInput): MessageProjection | null;
@@ -1,3 +1,4 @@
1
+ import { readViewerFile, referenceFileSourceForPath, referenceMediaTypeForPath, referenceTuttiGroupForPath, } from "../workspace-ops/index.js";
1
2
  import { createMainChatAgentMessage, hasMainChatWorklistFeedbackMessage, readCompletedWorklistFeedbackTaskIds, readTaskDetailProjection, readWorklistProjection, } from "../collaboration-state/index.js";
2
3
  import { mainChatInvalidates } from "./invalidations.js";
3
4
  const BODY_ITEM_LIMIT = 12;
@@ -27,6 +28,100 @@ function feedbackItemFromEntry(entry) {
27
28
  }
28
29
  return item;
29
30
  }
31
+ function userNoteCandidateFromEntry(entry) {
32
+ const trimmed = (entry.detail?.detail.result.user_note_candidate ?? "").trim();
33
+ return trimmed.length === 0 ? undefined : trimmed;
34
+ }
35
+ function normalizedUserNoteLine(value) {
36
+ return value.replace(/\s+/gu, " ").trim();
37
+ }
38
+ function finalNoteForCompletedEntries(entries) {
39
+ const notes = entries
40
+ .map(userNoteCandidateFromEntry)
41
+ .filter((note) => note !== undefined)
42
+ .map(normalizedUserNoteLine)
43
+ .filter((note) => note.length > 0);
44
+ if (notes.length === 0) {
45
+ return undefined;
46
+ }
47
+ if (entries.length === 1) {
48
+ return notes[0];
49
+ }
50
+ return `Completed work across ${entries.length} tasks: ${notes.join(" ")}`;
51
+ }
52
+ function shouldInspectGeneratedFiles(entry) {
53
+ const result = entry.detail?.detail.result;
54
+ return (result?.result_kind === "finished" &&
55
+ result.outcome === "completed" &&
56
+ result.promotion?.status === "promoted" &&
57
+ Array.isArray(result.changed_paths));
58
+ }
59
+ function generatedFileProjection(file) {
60
+ const projection = {
61
+ path: file.path,
62
+ name: file.name,
63
+ size_bytes: file.size_bytes,
64
+ };
65
+ const group = referenceTuttiGroupForPath(file.path);
66
+ if (group !== undefined) {
67
+ projection.group = group;
68
+ }
69
+ const mediaType = referenceMediaTypeForPath(file.path);
70
+ if (mediaType !== undefined) {
71
+ projection.media_type = mediaType;
72
+ }
73
+ return projection;
74
+ }
75
+ function readGeneratedFile(input) {
76
+ if (referenceFileSourceForPath(input.path) !== "tutti_output") {
77
+ return null;
78
+ }
79
+ try {
80
+ const file = readViewerFile({
81
+ workspaceRoot: input.workspaceRoot,
82
+ path: input.path,
83
+ maxBytes: 1,
84
+ now: input.now,
85
+ });
86
+ return generatedFileProjection(file.file);
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
92
+ function generatedFilesForCompletedEntries(input, entries) {
93
+ const workspaceRoot = input.workspaceRoot;
94
+ if (workspaceRoot === undefined) {
95
+ return [];
96
+ }
97
+ const paths = new Set();
98
+ for (const entry of entries) {
99
+ if (!shouldInspectGeneratedFiles(entry)) {
100
+ continue;
101
+ }
102
+ for (const path of entry.detail?.detail.result.changed_paths ?? []) {
103
+ if (referenceFileSourceForPath(path) === "tutti_output") {
104
+ paths.add(path);
105
+ }
106
+ }
107
+ }
108
+ return [...paths]
109
+ .map((path) => readGeneratedFile({
110
+ workspaceRoot,
111
+ path,
112
+ now: input.now,
113
+ }))
114
+ .filter((file) => file !== null)
115
+ .sort((left, right) => {
116
+ const groupOrder = (left.group ?? "").localeCompare(right.group ?? "", "en", {
117
+ sensitivity: "base",
118
+ });
119
+ if (groupOrder !== 0) {
120
+ return groupOrder;
121
+ }
122
+ return left.name.localeCompare(right.name, "en", { sensitivity: "base" });
123
+ });
124
+ }
30
125
  function feedbackCounters(entries) {
31
126
  const completed = entries.filter((entry) => entry.task.status === "done").length;
32
127
  const failed = entries.filter((entry) => entry.task.status === "failed").length;
@@ -51,6 +146,16 @@ function taskCountLabel(count) {
51
146
  }
52
147
  function markdownFallback(feedback) {
53
148
  const lines = [feedback.title, ""];
149
+ if (feedback.final_note !== undefined && feedback.final_note.trim().length > 0) {
150
+ lines.push(feedback.final_note.trim(), "");
151
+ }
152
+ if (feedback.generated_files !== undefined && feedback.generated_files.length > 0) {
153
+ lines.push("Generated files:");
154
+ for (const file of feedback.generated_files) {
155
+ lines.push(`- ${file.name}`);
156
+ }
157
+ lines.push("");
158
+ }
54
159
  const visibleItems = feedback.items.slice(0, BODY_ITEM_LIMIT);
55
160
  for (const item of visibleItems) {
56
161
  const prefix = item.status === "done" ? "- Done" : "- Failed";
@@ -102,6 +207,8 @@ export function emitWorklistCompletedFeedback(input) {
102
207
  return null;
103
208
  }
104
209
  const items = newCompletedEntries.map(feedbackItemFromEntry);
210
+ const finalNote = finalNoteForCompletedEntries(newCompletedEntries);
211
+ const generatedFiles = generatedFilesForCompletedEntries(input, newCompletedEntries);
105
212
  const feedback = {
106
213
  kind: "worklist_terminal",
107
214
  status: "completed",
@@ -110,6 +217,8 @@ export function emitWorklistCompletedFeedback(input) {
110
217
  feedback_key: completedFeedbackKey(items),
111
218
  counters: feedbackCounters(newCompletedEntries),
112
219
  items,
220
+ ...(finalNote === undefined ? {} : { final_note: finalNote }),
221
+ ...(generatedFiles.length === 0 ? {} : { generated_files: generatedFiles }),
113
222
  };
114
223
  return publishFeedbackMessage(input, feedback);
115
224
  }
@@ -332,6 +332,9 @@ async function runTaskAttempt(options, input) {
332
332
  if (changedPaths.length === 0 && taskOutput.decision === "completed_no_repo_changes") {
333
333
  return completedWithoutRepoChanges({
334
334
  summary: taskOutput.summary,
335
+ ...(taskOutput.user_note_candidate === undefined
336
+ ? {}
337
+ : { userNoteCandidate: taskOutput.user_note_candidate }),
335
338
  });
336
339
  }
337
340
  if (changedPaths.length === 0) {
@@ -424,6 +427,9 @@ async function runTaskAttempt(options, input) {
424
427
  checks,
425
428
  completion: "Promotion complete.",
426
429
  }),
430
+ ...(taskOutput.user_note_candidate === undefined
431
+ ? {}
432
+ : { user_note_candidate: taskOutput.user_note_candidate }),
427
433
  checks: checkStatus(checks),
428
434
  docs,
429
435
  ...checkSkipFields(checks),
@@ -1,12 +1,12 @@
1
1
  import type { RunPipelineInput, RunPipelineResult } from "../control-plane/index.js";
2
2
  import { type ResultCommit, type RunWorkspaceHandle } from "../workspace-ops/index.js";
3
3
  import type { OpenAiRunPipelineRunnerOptions } from "./openai.js";
4
- import type { TaskRunOutput } from "./task-run-output.js";
4
+ import type { CompletedNoRepoChangesTaskRunOutput, CompletedTaskRunOutput } from "./task-run-output.js";
5
5
  export declare function runPromotionReconcile(input: {
6
6
  options: OpenAiRunPipelineRunnerOptions;
7
7
  pipelineInput: RunPipelineInput;
8
8
  runHandle: RunWorkspaceHandle;
9
- taskOutput: TaskRunOutput;
9
+ taskOutput: CompletedTaskRunOutput | CompletedNoRepoChangesTaskRunOutput;
10
10
  sourceCommit: ResultCommit;
11
11
  changedPaths: string[];
12
12
  }): Promise<RunPipelineResult>;
@@ -93,6 +93,9 @@ export async function runPromotionReconcile(input) {
93
93
  checks,
94
94
  completion: "Promotion reconciled and complete.",
95
95
  }),
96
+ ...(input.taskOutput.user_note_candidate === undefined
97
+ ? {}
98
+ : { user_note_candidate: input.taskOutput.user_note_candidate }),
96
99
  checks: checkStatus(checks),
97
100
  docs,
98
101
  ...checkSkipFields(checks),
@@ -35,5 +35,6 @@ export declare function failedBeforeChecks(input: {
35
35
  }): RunPipelineResult;
36
36
  export declare function completedWithoutRepoChanges(input: {
37
37
  summary: string;
38
+ userNoteCandidate?: string;
38
39
  }): RunPipelineResult;
39
40
  //# sourceMappingURL=result-projections.d.ts.map
@@ -104,6 +104,9 @@ export function completedWithoutRepoChanges(input) {
104
104
  result_kind: "finished",
105
105
  outcome: "completed",
106
106
  summary: input.summary,
107
+ ...(input.userNoteCandidate === undefined
108
+ ? {}
109
+ : { user_note_candidate: input.userNoteCandidate }),
107
110
  checks: "skipped",
108
111
  docs: "checked_no_change",
109
112
  skip_reason_code: "validation_not_applicable",
@@ -5,10 +5,12 @@ export type InitialTaskRunOutput = {
5
5
  result: {
6
6
  completed: {
7
7
  summary: string;
8
+ user_note_candidate: string | null;
8
9
  };
9
10
  } | {
10
11
  completed_no_repo_changes: {
11
12
  summary: string;
13
+ user_note_candidate: string | null;
12
14
  };
13
15
  } | {
14
16
  needs_human: ClarificationRequestPayload;
@@ -18,13 +20,17 @@ export type InitialTaskRunOutput = {
18
20
  };
19
21
  };
20
22
  };
21
- export type TaskRunOutput = {
23
+ export type CompletedTaskRunOutput = {
22
24
  decision: "completed";
23
25
  summary: string;
24
- } | {
26
+ user_note_candidate?: string;
27
+ };
28
+ export type CompletedNoRepoChangesTaskRunOutput = {
25
29
  decision: "completed_no_repo_changes";
26
30
  summary: string;
27
- } | {
31
+ user_note_candidate?: string;
32
+ };
33
+ export type TaskRunOutput = CompletedTaskRunOutput | CompletedNoRepoChangesTaskRunOutput | {
28
34
  decision: "needs_human";
29
35
  summary: string;
30
36
  clarification_request_payload: ClarificationRequestPayload;
@@ -22,10 +22,13 @@ export const TASK_RUN_OUTPUT_SCHEMA = {
22
22
  properties: {
23
23
  completed: {
24
24
  type: "object",
25
- required: ["summary"],
25
+ required: ["summary", "user_note_candidate"],
26
26
  additionalProperties: false,
27
27
  properties: {
28
28
  summary: { type: "string" },
29
+ user_note_candidate: {
30
+ anyOf: [{ type: "string" }, { type: "null" }],
31
+ },
29
32
  },
30
33
  },
31
34
  },
@@ -37,10 +40,13 @@ export const TASK_RUN_OUTPUT_SCHEMA = {
37
40
  properties: {
38
41
  completed_no_repo_changes: {
39
42
  type: "object",
40
- required: ["summary"],
43
+ required: ["summary", "user_note_candidate"],
41
44
  additionalProperties: false,
42
45
  properties: {
43
46
  summary: { type: "string" },
47
+ user_note_candidate: {
48
+ anyOf: [{ type: "string" }, { type: "null" }],
49
+ },
44
50
  },
45
51
  },
46
52
  },
@@ -96,6 +102,16 @@ function isSummaryRecord(value) {
96
102
  Object.keys(value).every((key) => key === "summary") &&
97
103
  typeof value.summary === "string");
98
104
  }
105
+ function isCompletedRecord(value) {
106
+ return (isRecord(value) &&
107
+ Object.keys(value).every((key) => key === "summary" || key === "user_note_candidate") &&
108
+ typeof value.summary === "string" &&
109
+ (typeof value.user_note_candidate === "string" || value.user_note_candidate === null));
110
+ }
111
+ function normalizedUserNoteCandidate(value) {
112
+ const trimmed = (value ?? "").trim();
113
+ return trimmed.length === 0 ? undefined : trimmed;
114
+ }
99
115
  export function isInitialTaskRunOutput(value) {
100
116
  if (!isRecord(value) || !isRecord(value.result) || Object.keys(value).length !== 1) {
101
117
  return false;
@@ -105,10 +121,10 @@ export function isInitialTaskRunOutput(value) {
105
121
  return false;
106
122
  }
107
123
  if ("completed" in value.result) {
108
- return isSummaryRecord(value.result.completed);
124
+ return isCompletedRecord(value.result.completed);
109
125
  }
110
126
  if ("completed_no_repo_changes" in value.result) {
111
- return isSummaryRecord(value.result.completed_no_repo_changes);
127
+ return isCompletedRecord(value.result.completed_no_repo_changes);
112
128
  }
113
129
  if ("needs_human" in value.result) {
114
130
  return isTaskRunClarificationRequestPayload(value.result.needs_human);
@@ -120,15 +136,19 @@ export function isInitialTaskRunOutput(value) {
120
136
  }
121
137
  export function normalizeTaskRunOutput(output) {
122
138
  if ("completed" in output.result) {
139
+ const userNoteCandidate = normalizedUserNoteCandidate(output.result.completed.user_note_candidate);
123
140
  return {
124
141
  decision: "completed",
125
142
  summary: output.result.completed.summary,
143
+ ...(userNoteCandidate === undefined ? {} : { user_note_candidate: userNoteCandidate }),
126
144
  };
127
145
  }
128
146
  if ("completed_no_repo_changes" in output.result) {
147
+ const userNoteCandidate = normalizedUserNoteCandidate(output.result.completed_no_repo_changes.user_note_candidate);
129
148
  return {
130
149
  decision: "completed_no_repo_changes",
131
150
  summary: output.result.completed_no_repo_changes.summary,
151
+ ...(userNoteCandidate === undefined ? {} : { user_note_candidate: userNoteCandidate }),
132
152
  };
133
153
  }
134
154
  if ("needs_human" in output.result) {
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { formatCliError, formatCliErrorReason } from "./errors.js";
2
+ import { formatCliError } from "./errors.js";
3
3
  import { parseCliArgs } from "./args.js";
4
4
  import { runBackgroundLaunchCommand, runForegroundLaunchCommand, runInternalHostRunCommand, } from "./launch-command.js";
5
5
  import { resolveExistingProjectContext } from "./project-resolver.js";
6
6
  import { runProviderSetupTui } from "./provider-tui.js";
7
- import { runInviteCommand, runLogsCommand, runProviderStatusCommand, runPsManageCommand, runPsCommand, runStopCommand, } from "./runtime-commands.js";
7
+ import { runDoctorCommand, runInviteCommand, runLogsCommand, runProviderStatusCommand, runPsManageCommand, runPsCommand, runStopCommand, } from "./runtime-commands.js";
8
8
  import { readCliVersion } from "./version.js";
9
9
  const HELP_TEXT = `Usage:
10
10
  tutti launch [target] [--yes] [--foreground]
@@ -108,25 +108,6 @@ async function runProviderSetupCommand(target) {
108
108
  ].join("\n"));
109
109
  process.stdout.write("\n");
110
110
  }
111
- function runDoctorCommand(target) {
112
- const checks = [];
113
- checks.push(`Node: ${process.version}`);
114
- checks.push(`Tutti: ${readCliVersion()}`);
115
- try {
116
- const project = resolveExistingProjectContext({
117
- ...(target === undefined ? {} : { target }),
118
- });
119
- checks.push(`Project: ${project.display_name}`);
120
- checks.push(`Project ID: ${project.project_id}`);
121
- checks.push(`Provider: ${project.provider_config.status}`);
122
- checks.push(`Host: ${project.runtime_endpoint === null ? "not running" : project.runtime_endpoint.base_url}`);
123
- }
124
- catch (error) {
125
- checks.push("Project: unavailable");
126
- checks.push(`Project detail: ${formatCliErrorReason(error)}`);
127
- }
128
- process.stdout.write(`${checks.join("\n")}\n`);
129
- }
130
111
  try {
131
112
  const command = parseCliArgs(process.argv.slice(2));
132
113
  switch (command.kind) {
@@ -171,7 +152,7 @@ try {
171
152
  process.stdout.write(`${runProviderStatusCommand(command.target)}\n`);
172
153
  break;
173
154
  case "doctor":
174
- runDoctorCommand(command.target);
155
+ process.stdout.write(`${runDoctorCommand(command.target)}\n`);
175
156
  break;
176
157
  }
177
158
  }
@@ -61,6 +61,10 @@ export declare function deleteMachineRuntimeEndpoint(options: {
61
61
  projectId: ProjectId;
62
62
  expectedToken?: string;
63
63
  }): boolean;
64
+ export declare function deleteMachineProjectLocalState(options: {
65
+ tuttiHome: string;
66
+ projectId: ProjectId;
67
+ }): boolean;
64
68
  export declare function readHostRegistrationSecret(tuttiHome: string, projectId: ProjectId): HostRegistrationSecretFile;
65
69
  export declare function updateMachineProjectRelayBinding(options: {
66
70
  tuttiHome: string;
@@ -1,5 +1,5 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
2
+ import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { ID_PREFIXES, isPrefixedId } from "@tutti/shared/ids";
5
5
  import { LaunchError } from "./errors.js";
@@ -150,6 +150,20 @@ export function deleteMachineRuntimeEndpoint(options) {
150
150
  unlinkSync(endpointPath);
151
151
  return true;
152
152
  }
153
+ export function deleteMachineProjectLocalState(options) {
154
+ let deleted = false;
155
+ const localStoreRoot = getProjectLocalStoreRoot(options.tuttiHome, options.projectId);
156
+ if (existsSync(localStoreRoot)) {
157
+ rmSync(localStoreRoot, { recursive: true, force: true });
158
+ deleted = true;
159
+ }
160
+ const secretPath = getHostRegistrationSecretPath(options.tuttiHome, options.projectId);
161
+ if (existsSync(secretPath)) {
162
+ unlinkSync(secretPath);
163
+ deleted = true;
164
+ }
165
+ return deleted;
166
+ }
153
167
  export function readHostRegistrationSecret(tuttiHome, projectId) {
154
168
  const readResult = readJsonFile(getHostRegistrationSecretPath(tuttiHome, projectId));
155
169
  if (readResult.kind === "missing") {
@@ -11,6 +11,9 @@ export type ExistingProjectContext = {
11
11
  provider_config: OpenAiProviderConfigProjection;
12
12
  provider_base_url?: string;
13
13
  };
14
+ export type ManagedProjectContext = ExistingProjectContext & {
15
+ workspace_exists: boolean;
16
+ };
14
17
  export declare function resolveLaunchWorkspacePath(options: {
15
18
  target?: string;
16
19
  cwd?: string;
@@ -22,4 +25,9 @@ export declare function resolveExistingProjectContext(options: {
22
25
  cwd?: string;
23
26
  env?: NodeJS.ProcessEnv;
24
27
  }): ExistingProjectContext;
28
+ export declare function resolveManagedProjectContext(options: {
29
+ target?: string;
30
+ cwd?: string;
31
+ env?: NodeJS.ProcessEnv;
32
+ }): ManagedProjectContext;
25
33
  //# sourceMappingURL=project-resolver.d.ts.map