@xfey/tutti 0.1.28 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/collaboration-state/index.d.ts +1 -1
- package/dist/collaboration-state/index.js +1 -1
- package/dist/collaboration-state/messages.d.ts +1 -0
- package/dist/collaboration-state/messages.js +12 -0
- package/dist/control-plane/index.d.ts +0 -1
- package/dist/control-plane/index.js +16 -49
- package/dist/control-plane/task-compile-output.js +2 -54
- package/dist/control-plane/worklist-terminal-feedback.d.ts +9 -1
- package/dist/control-plane/worklist-terminal-feedback.js +99 -1
- package/dist/server-shell/cli/args.d.ts +9 -0
- package/dist/server-shell/cli/args.js +55 -5
- package/dist/server-shell/cli/cli.js +46 -7
- package/dist/server-shell/cli/errors.d.ts +1 -1
- package/dist/server-shell/cli/errors.js +6 -1
- package/dist/server-shell/cli/host-lifecycle.d.ts +1 -0
- package/dist/server-shell/cli/host-lifecycle.js +155 -41
- package/dist/server-shell/cli/host-relay-connection-manager.d.ts +50 -0
- package/dist/server-shell/cli/host-relay-connection-manager.js +447 -0
- package/dist/server-shell/cli/host-relay-status.d.ts +4 -0
- package/dist/server-shell/cli/host-relay-status.js +74 -15
- package/dist/server-shell/cli/host-runtime-endpoint.js +26 -0
- package/dist/server-shell/cli/host-server-runtime.d.ts +2 -2
- package/dist/server-shell/cli/host-server-runtime.js +19 -15
- package/dist/server-shell/cli/launch-command.d.ts +1 -0
- package/dist/server-shell/cli/launch-command.js +8 -10
- package/dist/server-shell/cli/launch.d.ts +1 -0
- package/dist/server-shell/cli/launch.js +7 -3
- package/dist/server-shell/cli/local-control-client.d.ts +5 -0
- package/dist/server-shell/cli/local-control-client.js +9 -0
- package/dist/server-shell/cli/machine-local.d.ts +8 -0
- package/dist/server-shell/cli/machine-local.js +29 -0
- package/dist/server-shell/cli/machine-project-inspector.d.ts +81 -0
- package/dist/server-shell/cli/machine-project-inspector.js +205 -0
- package/dist/server-shell/cli/project-resolver.js +22 -32
- package/dist/server-shell/cli/relay-registration.d.ts +5 -2
- package/dist/server-shell/cli/relay-registration.js +7 -3
- package/dist/server-shell/cli/runtime-commands.d.ts +30 -4
- package/dist/server-shell/cli/runtime-commands.js +230 -113
- package/dist/server-shell/http/routes/local-control.d.ts +10 -0
- package/dist/server-shell/http/routes/local-control.js +29 -0
- package/migrations/README.md +1 -1
- package/node_modules/@tutti/relay-client/dist/host-control.d.ts +23 -0
- package/node_modules/@tutti/relay-client/dist/host-control.js +70 -3
- package/node_modules/@tutti/relay-client/dist/tunnel-types.d.ts +6 -2
- package/node_modules/@tutti/relay-client/dist/tunnel.js +13 -2
- package/node_modules/@tutti/shared/dist/schemas/api/clarifications.d.ts +21 -0
- package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
- package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
- package/node_modules/@tutti/shared/dist/schemas/api/messages.d.ts +105 -0
- package/node_modules/@tutti/shared/dist/schemas/api/messages.js +16 -0
- package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +2 -1
- package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.d.ts +21 -0
- package/package.json +1 -1
- package/prompts/prompt-flow-map.md +1 -1
- package/prompts/runs/README.md +1 -1
- package/prompts/runs/task-continuation.md +1 -1
- package/prompts/runs/task-retry.md +1 -1
- package/prompts/runs/task-run.md +1 -1
- package/web/assets/index-BHRlhFwi.css +1 -0
- package/web/assets/index-DPU2uqZl.js +29 -0
- package/web/index.html +2 -2
- package/web/assets/index-CeGTRkQn.css +0 -1
- package/web/assets/index-DGnDFv9O.js +0 -29
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
- 已具备 Fastify host shell、后台 host manager CLI 套件、Dev Runner、Host Project API、SSE、SQLite store、collaboration-state、workspace read-state、project timeline observability helper、Control Plane、Procedure / Run、workspace-ops、checks、Relay staged upload finalize、user skills 文件服务、artifacts 可视化产物查看层和 OpenAI / Codex provider 接入。
|
|
28
28
|
- Host server 支持在显式 `webStatic.root` 或 packaged `web/` 存在时服务 Web 静态产物;`/api/*`、SSE、health 和 host-local control 不被 SPA fallback 吞掉。
|
|
29
29
|
- Root `pack:dry-run` 会先清理 build output、重新构建,再复制 `apps/web/dist` 到 package-local `web/`,生成临时 staging `@xfey/tutti` artifact,并校验 npm pack 文件列表包含 CLI、migrations、prompts、Web index、bundled `@tutti/shared` / `@tutti/relay-client` 以及它们的外部 runtime dependencies,同时排除源码、source map、日志、SQLite、`.env`、`.tutti-dev` 与测试产物。
|
|
30
|
-
- Root `smoke:packed-cli` 同样从干净 build output 生成 staging `@xfey/tutti` tarball、在临时目录安装,并验证 SQLite native binding、`tutti --help` / `tutti launch --help` / `tutti --version`、migrations、prompts、Web static index、bundled internal dependencies、bundled external runtime dependencies、CLI QR dependency、仓库锁定的 `@openai/codex` binary 可执行性和临时全局安装路径;该 smoke 不需要 Tutti source repo 存在。
|
|
30
|
+
- Root `smoke:packed-cli` 同样从干净 build output 生成 staging `@xfey/tutti` tarball、在临时目录安装,并验证 SQLite native binding、`tutti --help` / `tutti launch --help` / `tutti archive --help` / `tutti --version`、migrations、prompts、Web static index、bundled internal dependencies、bundled external runtime dependencies、CLI QR dependency、仓库锁定的 `@openai/codex` binary 可执行性和临时全局安装路径;该 smoke 不需要 Tutti source repo 存在。
|
|
31
31
|
- `prompts/` 是所有模型-facing prompt、指令模板和 Codex 执行模板的统一落点;长 prompt 不内联在 TypeScript 中。
|
|
32
32
|
- Host Project API 只信任 Relay tunnel metadata 注入的 `relay_session_context`;direct loopback 缺少可信 context 时不作为浏览器工作区入口。
|
|
33
33
|
- 详细模块落点见 `src/README.md`;logger redaction、command idempotency、schema、provider 与 Codex 边界以 `docs/system/` 对应 canonical 文档为准。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { CollaborationStateCursorError } from "./serialization.js";
|
|
2
2
|
export { createIdleExecutionStatusProjection } from "./execution-status.js";
|
|
3
|
-
export { createMainChatAgentMessage, createMainChatMessage, createMainChatSystemMessage, hasMainChatArtifactPreviewMessage, hasMainChatWorklistCompileMessage, hasMainChatWorklistFeedbackMessage, readCompletedWorklistFeedbackTaskIds, readMessageProjectionById, readMainChatMessageWindow, readMainChatMessagesAfter, readMainChatMessagesAfterCursor, readMainChatMessagesBeforeCursor, } from "./messages.js";
|
|
3
|
+
export { createMainChatAgentMessage, createMainChatMessage, createMainChatSystemMessage, hasMainChatArtifactPreviewMessage, hasMainChatTaskResultMessage, hasMainChatWorklistCompileMessage, hasMainChatWorklistFeedbackMessage, readCompletedWorklistFeedbackTaskIds, readMessageProjectionById, readMainChatMessageWindow, readMainChatMessagesAfter, readMainChatMessagesAfterCursor, readMainChatMessagesBeforeCursor, } from "./messages.js";
|
|
4
4
|
export { createEmptyScratchpadProjection, readScratchpadReceiptSourceRefs, readScratchpadProjection, submitScratchpadProjection, upsertScratchpadProjection, } from "./scratchpad.js";
|
|
5
5
|
export { advanceScratchpadTaskCompileSourceCursor, markScratchpadRefreshFailed, markScratchpadRefreshStarted, markScratchpadRefreshSucceeded, markScratchpadSourceDirty, readScratchpadSourceState, } from "./scratchpad-source-state.js";
|
|
6
6
|
export { deleteStaleClarificationSubmitReplayHints, readActiveClarificationProjection, readClarificationRoundProjection, readClarificationRoundsForThread, readClarificationThreadResponse, readSealedClarificationRoundsWithoutSuccessor, readSubmittedTaskCompileClarificationRounds, readSubmittedTaskCompileClarificationRoundsAfterCursor, } from "./clarification-records.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { CollaborationStateCursorError } from "./serialization.js";
|
|
2
2
|
export { createIdleExecutionStatusProjection } from "./execution-status.js";
|
|
3
|
-
export { createMainChatAgentMessage, createMainChatMessage, createMainChatSystemMessage, hasMainChatArtifactPreviewMessage, hasMainChatWorklistCompileMessage, hasMainChatWorklistFeedbackMessage, readCompletedWorklistFeedbackTaskIds, readMessageProjectionById, readMainChatMessageWindow, readMainChatMessagesAfter, readMainChatMessagesAfterCursor, readMainChatMessagesBeforeCursor, } from "./messages.js";
|
|
3
|
+
export { createMainChatAgentMessage, createMainChatMessage, createMainChatSystemMessage, hasMainChatArtifactPreviewMessage, hasMainChatTaskResultMessage, hasMainChatWorklistCompileMessage, hasMainChatWorklistFeedbackMessage, readCompletedWorklistFeedbackTaskIds, readMessageProjectionById, readMainChatMessageWindow, readMainChatMessagesAfter, readMainChatMessagesAfterCursor, readMainChatMessagesBeforeCursor, } from "./messages.js";
|
|
4
4
|
export { createEmptyScratchpadProjection, readScratchpadReceiptSourceRefs, readScratchpadProjection, submitScratchpadProjection, upsertScratchpadProjection, } from "./scratchpad.js";
|
|
5
5
|
export { advanceScratchpadTaskCompileSourceCursor, markScratchpadRefreshFailed, markScratchpadRefreshStarted, markScratchpadRefreshSucceeded, markScratchpadSourceDirty, readScratchpadSourceState, } from "./scratchpad-source-state.js";
|
|
6
6
|
export { deleteStaleClarificationSubmitReplayHints, readActiveClarificationProjection, readClarificationRoundProjection, readClarificationRoundsForThread, readClarificationThreadResponse, readSealedClarificationRoundsWithoutSuccessor, readSubmittedTaskCompileClarificationRounds, readSubmittedTaskCompileClarificationRoundsAfterCursor, } from "./clarification-records.js";
|
|
@@ -9,6 +9,7 @@ export declare function createMainChatSystemMessage(db: SqliteDatabase, input: C
|
|
|
9
9
|
export declare function createMainChatAgentMessage(db: SqliteDatabase, input: CreateMainChatAgentMessageInput): MessageProjection;
|
|
10
10
|
export declare function readMessageProjectionById(db: SqliteDatabase, messageId: MessageId): MessageProjection | null;
|
|
11
11
|
export declare function hasMainChatWorklistFeedbackMessage(db: SqliteDatabase, feedbackKey: string): boolean;
|
|
12
|
+
export declare function hasMainChatTaskResultMessage(db: SqliteDatabase, resultKey: string): boolean;
|
|
12
13
|
export declare function hasMainChatWorklistCompileMessage(db: SqliteDatabase, workflowRef: WorkflowInvocationRef): boolean;
|
|
13
14
|
export declare function hasMainChatArtifactPreviewMessage(db: SqliteDatabase, runResultId: RunResultId): boolean;
|
|
14
15
|
export declare function readCompletedWorklistFeedbackTaskIds(db: SqliteDatabase): Set<TaskId>;
|
|
@@ -207,6 +207,18 @@ export function hasMainChatWorklistFeedbackMessage(db, feedbackKey) {
|
|
|
207
207
|
.get(feedbackKey);
|
|
208
208
|
return row !== undefined;
|
|
209
209
|
}
|
|
210
|
+
export function hasMainChatTaskResultMessage(db, resultKey) {
|
|
211
|
+
const row = db
|
|
212
|
+
.prepare(`
|
|
213
|
+
SELECT 1 AS exists_flag
|
|
214
|
+
FROM messages
|
|
215
|
+
WHERE scope_kind = 'main_chat'
|
|
216
|
+
AND json_extract(refs_json, '$.task_result.result_key') = ?
|
|
217
|
+
LIMIT 1
|
|
218
|
+
`)
|
|
219
|
+
.get(resultKey);
|
|
220
|
+
return row !== undefined;
|
|
221
|
+
}
|
|
210
222
|
export function hasMainChatWorklistCompileMessage(db, workflowRef) {
|
|
211
223
|
const row = db
|
|
212
224
|
.prepare(`
|
|
@@ -48,7 +48,6 @@ export declare class Phase5ControlPlane {
|
|
|
48
48
|
private readScratchpadRefreshBlocker;
|
|
49
49
|
private publishTaskRunStarted;
|
|
50
50
|
private publishRunResultRecorded;
|
|
51
|
-
private maybePublishArtifactPreviewMessage;
|
|
52
51
|
private publishRunTransition;
|
|
53
52
|
private handleRunTerminalTransition;
|
|
54
53
|
private tryRunTaskArrivalScheduler;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readWorklistProjection } from "../collaboration-state/index.js";
|
|
2
2
|
import { ProcedureEngine } from "../procedure-engine/index.js";
|
|
3
3
|
import { RunEngine } from "../run-engine/index.js";
|
|
4
4
|
import { publishProcedureTransition as publishProcedureTransitionEvent, publishRunResultRecorded as publishRunResultRecordedEvent, publishRunTransition as publishRunTransitionEvent, publishTaskRunStarted as publishTaskRunStartedEvent, } from "./event-publishers.js";
|
|
@@ -15,11 +15,7 @@ import { startControlPlaneTaskBoundFollowUpCheck } from "./follow-up-start.js";
|
|
|
15
15
|
import { runControlPlaneSchedulerForTaskArrival, runControlPlaneSchedulerNow, } from "./run-scheduler.js";
|
|
16
16
|
import { startControlPlaneRunForTask } from "./run-start.js";
|
|
17
17
|
import { runSendClarificationRoundMessage, runSubmitClarificationRound, } from "./clarification-commands.js";
|
|
18
|
-
import {
|
|
19
|
-
import { mainChatInvalidates } from "./invalidations.js";
|
|
20
|
-
function artifactPreviewChangedPaths(paths) {
|
|
21
|
-
return paths?.some((path) => path === "tutti.artifact.json") ?? false;
|
|
22
|
-
}
|
|
18
|
+
import { emitTaskCompletedResultMessage, emitTaskFailedResultMessage, } from "./worklist-terminal-feedback.js";
|
|
23
19
|
export class Phase5ControlPlane {
|
|
24
20
|
engine;
|
|
25
21
|
runEngine;
|
|
@@ -274,40 +270,6 @@ export class Phase5ControlPlane {
|
|
|
274
270
|
}
|
|
275
271
|
publishRunResultRecorded(recorded) {
|
|
276
272
|
publishRunResultRecordedEvent(this.events, recorded);
|
|
277
|
-
this.maybePublishArtifactPreviewMessage(recorded);
|
|
278
|
-
}
|
|
279
|
-
maybePublishArtifactPreviewMessage(recorded) {
|
|
280
|
-
const result = recorded.task_detail.detail.result;
|
|
281
|
-
if (result.promotion?.status !== "promoted" ||
|
|
282
|
-
!artifactPreviewChangedPaths(result.changed_paths)) {
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
const runResultId = recorded.run_result.id;
|
|
286
|
-
if (hasMainChatArtifactPreviewMessage(this.store.db, runResultId)) {
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
const title = "Preview updated";
|
|
290
|
-
const message = createMainChatAgentMessage(this.store.db, {
|
|
291
|
-
body: `${title}\n\nThe current artifact preview was updated by "${recorded.task.title}". Open Artifacts to review the latest preview.`,
|
|
292
|
-
refs: {
|
|
293
|
-
scratchpad_source: "exclude",
|
|
294
|
-
task_id: recorded.task.id,
|
|
295
|
-
activity_ref: recorded.run_result.activity_ref,
|
|
296
|
-
artifact_preview: {
|
|
297
|
-
kind: "artifact_preview",
|
|
298
|
-
title,
|
|
299
|
-
status: "updated",
|
|
300
|
-
task_id: recorded.task.id,
|
|
301
|
-
run_result_id: runResultId,
|
|
302
|
-
},
|
|
303
|
-
},
|
|
304
|
-
now: this.now,
|
|
305
|
-
});
|
|
306
|
-
this.events.publish({
|
|
307
|
-
event_type: "message.created",
|
|
308
|
-
payload: { message },
|
|
309
|
-
invalidates: mainChatInvalidates(),
|
|
310
|
-
});
|
|
311
273
|
}
|
|
312
274
|
publishRunTransition(state) {
|
|
313
275
|
publishRunTransitionEvent({
|
|
@@ -321,14 +283,27 @@ export class Phase5ControlPlane {
|
|
|
321
283
|
}
|
|
322
284
|
handleRunTerminalTransition(state) {
|
|
323
285
|
if (state.activity.status === "finished") {
|
|
286
|
+
if (state.activity.task_id !== undefined) {
|
|
287
|
+
emitTaskCompletedResultMessage({
|
|
288
|
+
store: this.store,
|
|
289
|
+
events: this.events,
|
|
290
|
+
taskId: state.activity.task_id,
|
|
291
|
+
activityRef: state.activity.activity_ref,
|
|
292
|
+
...(this.projectContext === undefined
|
|
293
|
+
? {}
|
|
294
|
+
: { workspaceRoot: this.projectContext.workspaceRoot }),
|
|
295
|
+
now: this.now,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
324
298
|
this.maybeScheduleAfterRunCompletion();
|
|
325
299
|
return;
|
|
326
300
|
}
|
|
327
301
|
if (state.activity.status === "failed" && state.activity.task_id !== undefined) {
|
|
328
|
-
|
|
302
|
+
emitTaskFailedResultMessage({
|
|
329
303
|
store: this.store,
|
|
330
304
|
events: this.events,
|
|
331
305
|
taskId: state.activity.task_id,
|
|
306
|
+
activityRef: state.activity.activity_ref,
|
|
332
307
|
now: this.now,
|
|
333
308
|
});
|
|
334
309
|
}
|
|
@@ -348,14 +323,6 @@ export class Phase5ControlPlane {
|
|
|
348
323
|
if (this.tryRunTaskArrivalScheduler()) {
|
|
349
324
|
return;
|
|
350
325
|
}
|
|
351
|
-
emitWorklistCompletedFeedback({
|
|
352
|
-
store: this.store,
|
|
353
|
-
events: this.events,
|
|
354
|
-
...(this.projectContext === undefined
|
|
355
|
-
? {}
|
|
356
|
-
: { workspaceRoot: this.projectContext.workspaceRoot }),
|
|
357
|
-
now: this.now,
|
|
358
|
-
});
|
|
359
326
|
this.maybeStartContextSyncAfterWorklistCompletion();
|
|
360
327
|
}
|
|
361
328
|
worklistCompletionContextSyncTrigger() {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { applyTaskCompileProposal,
|
|
2
|
-
import { clarificationInvalidates,
|
|
1
|
+
import { applyTaskCompileProposal, openNextTaskCompileClarificationRound, openTaskCompileClarificationRound, readWorklistProjection, submitScratchpadProjection, } from "../collaboration-state/index.js";
|
|
2
|
+
import { clarificationInvalidates, taskCompileInvalidates, } from "./invalidations.js";
|
|
3
3
|
import { normalizeTaskCompileProposal } from "./task-proposal.js";
|
|
4
4
|
const EMPTY_INITIAL_WORKLIST_CLARIFICATION = {
|
|
5
5
|
title: "Worklist scope needed",
|
|
@@ -9,51 +9,6 @@ const EMPTY_INITIAL_WORKLIST_CLARIFICATION = {
|
|
|
9
9
|
function countWorklistTasks(worklist) {
|
|
10
10
|
return worklist.modules.reduce((count, module) => count + module.tasks.length, 0);
|
|
11
11
|
}
|
|
12
|
-
function taskCountLabel(count) {
|
|
13
|
-
return count === 1 ? "1 task" : `${count} tasks`;
|
|
14
|
-
}
|
|
15
|
-
function worklistCompileFallback(input) {
|
|
16
|
-
const lines = [input.title, ""];
|
|
17
|
-
for (const task of input.tasks) {
|
|
18
|
-
lines.push(`- ${task.title} (${task.module_name})`);
|
|
19
|
-
if (task.summary.trim().length > 0) {
|
|
20
|
-
lines.push(` ${task.summary}`);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
lines.push("", "Open Worklist to review the compiled tasks.");
|
|
24
|
-
return lines.join("\n");
|
|
25
|
-
}
|
|
26
|
-
function publishWorklistCompileMessage(input) {
|
|
27
|
-
if (hasMainChatWorklistCompileMessage(input.store.db, input.workflowRef)) {
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
const tasks = readCompiledTaskSummariesByWorkflowRef(input.store.db, input.workflowRef);
|
|
31
|
-
if (tasks.length === 0) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
const title = `Compiled ${taskCountLabel(tasks.length)} into Worklist`;
|
|
35
|
-
const message = createMainChatAgentMessage(input.store.db, {
|
|
36
|
-
body: worklistCompileFallback({ title, tasks }),
|
|
37
|
-
refs: {
|
|
38
|
-
scratchpad_source: "exclude",
|
|
39
|
-
workflow_ref: input.workflowRef,
|
|
40
|
-
activity_ref: input.activityRef,
|
|
41
|
-
worklist_compile: {
|
|
42
|
-
kind: "worklist_compile",
|
|
43
|
-
workflow_ref: input.workflowRef,
|
|
44
|
-
title,
|
|
45
|
-
task_count: tasks.length,
|
|
46
|
-
tasks,
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
now: input.now,
|
|
50
|
-
});
|
|
51
|
-
input.events.publish({
|
|
52
|
-
event_type: "message.created",
|
|
53
|
-
payload: { message },
|
|
54
|
-
invalidates: mainChatInvalidates(),
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
12
|
function openTaskCompileNeedsHuman(input) {
|
|
58
13
|
const opened = input.continueFromRoundId === undefined
|
|
59
14
|
? openTaskCompileClarificationRound(input.store.db, {
|
|
@@ -155,13 +110,6 @@ export function applyTaskCompileProcedureOutput(input) {
|
|
|
155
110
|
},
|
|
156
111
|
invalidates: taskCompileInvalidates(),
|
|
157
112
|
});
|
|
158
|
-
publishWorklistCompileMessage({
|
|
159
|
-
store: input.store,
|
|
160
|
-
events: input.events,
|
|
161
|
-
workflowRef: input.workflowRef,
|
|
162
|
-
activityRef: input.activityRef,
|
|
163
|
-
now: input.now,
|
|
164
|
-
});
|
|
165
113
|
if (input.continueFromRoundId !== undefined) {
|
|
166
114
|
return {
|
|
167
115
|
kind: "completed",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TaskId } from "@tutti/shared/ids";
|
|
1
|
+
import type { ActivityRef, TaskId } from "@tutti/shared/ids";
|
|
2
2
|
import type { MessageProjection } from "@tutti/shared/schemas/api";
|
|
3
3
|
import type { HostProjectStore } from "../store/index.js";
|
|
4
4
|
import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
|
|
@@ -8,6 +8,14 @@ type EmitWorklistFeedbackInput = {
|
|
|
8
8
|
workspaceRoot?: string;
|
|
9
9
|
now: () => Date;
|
|
10
10
|
};
|
|
11
|
+
export declare function emitTaskCompletedResultMessage(input: EmitWorklistFeedbackInput & {
|
|
12
|
+
taskId: TaskId;
|
|
13
|
+
activityRef?: ActivityRef;
|
|
14
|
+
}): MessageProjection | null;
|
|
15
|
+
export declare function emitTaskFailedResultMessage(input: EmitWorklistFeedbackInput & {
|
|
16
|
+
taskId: TaskId;
|
|
17
|
+
activityRef?: ActivityRef;
|
|
18
|
+
}): MessageProjection | null;
|
|
11
19
|
export declare function emitWorklistCompletedFeedback(input: EmitWorklistFeedbackInput): MessageProjection | null;
|
|
12
20
|
export declare function emitWorklistFailedFeedback(input: EmitWorklistFeedbackInput & {
|
|
13
21
|
taskId: TaskId;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readViewerFile, referenceFileSourceForPath, referenceMediaTypeForPath, referenceTuttiGroupForPath, } from "../workspace-ops/index.js";
|
|
2
|
-
import { createMainChatAgentMessage, hasMainChatWorklistFeedbackMessage, readCompletedWorklistFeedbackTaskIds, readTaskDetailProjection, readWorklistProjection, } from "../collaboration-state/index.js";
|
|
2
|
+
import { createMainChatAgentMessage, hasMainChatTaskResultMessage, hasMainChatWorklistFeedbackMessage, readCompletedWorklistFeedbackTaskIds, readTaskDetailProjection, readWorklistProjection, } from "../collaboration-state/index.js";
|
|
3
3
|
import { mainChatInvalidates } from "./invalidations.js";
|
|
4
4
|
const BODY_ITEM_LIMIT = 12;
|
|
5
5
|
function summaryText(value, fallback) {
|
|
@@ -177,6 +177,104 @@ function markdownFallback(feedback) {
|
|
|
177
177
|
}
|
|
178
178
|
return lines.join("\n");
|
|
179
179
|
}
|
|
180
|
+
function taskResultKey(item) {
|
|
181
|
+
return `task-result:${item.status}:${item.task_id}:${item.latest_run_result_id ?? "no-result"}`;
|
|
182
|
+
}
|
|
183
|
+
function taskResultFallback(result) {
|
|
184
|
+
const moduleText = result.module_name === undefined ? "" : ` (${result.module_name})`;
|
|
185
|
+
const lines = [result.title, "", `${result.task_title}${moduleText}`, ""];
|
|
186
|
+
if (result.final_note !== undefined && result.final_note.trim().length > 0) {
|
|
187
|
+
lines.push(result.final_note.trim(), "");
|
|
188
|
+
}
|
|
189
|
+
if (result.generated_files !== undefined && result.generated_files.length > 0) {
|
|
190
|
+
lines.push("Generated files:");
|
|
191
|
+
for (const file of result.generated_files) {
|
|
192
|
+
lines.push(`- ${file.name}`);
|
|
193
|
+
}
|
|
194
|
+
lines.push("");
|
|
195
|
+
}
|
|
196
|
+
if (result.summary.trim().length > 0) {
|
|
197
|
+
lines.push(result.summary.trim(), "");
|
|
198
|
+
}
|
|
199
|
+
if (result.status === "failed") {
|
|
200
|
+
lines.push("Review the Worklist task detail for the failure context.");
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
lines.push("Open the Worklist task detail to review the result.");
|
|
204
|
+
}
|
|
205
|
+
return lines.join("\n");
|
|
206
|
+
}
|
|
207
|
+
function publishTaskResultMessage(input, result) {
|
|
208
|
+
if (hasMainChatTaskResultMessage(input.store.db, result.result_key)) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const message = createMainChatAgentMessage(input.store.db, {
|
|
212
|
+
body: taskResultFallback(result),
|
|
213
|
+
refs: {
|
|
214
|
+
scratchpad_source: "exclude",
|
|
215
|
+
task_id: result.task_id,
|
|
216
|
+
...(result.activity_ref === undefined ? {} : { activity_ref: result.activity_ref }),
|
|
217
|
+
task_result: result,
|
|
218
|
+
},
|
|
219
|
+
now: input.now,
|
|
220
|
+
});
|
|
221
|
+
input.events.publish({
|
|
222
|
+
event_type: "message.created",
|
|
223
|
+
payload: { message },
|
|
224
|
+
invalidates: mainChatInvalidates(),
|
|
225
|
+
});
|
|
226
|
+
return message;
|
|
227
|
+
}
|
|
228
|
+
export function emitTaskCompletedResultMessage(input) {
|
|
229
|
+
const entry = readWorklistTaskEntries(input.store).find((item) => item.task.id === input.taskId);
|
|
230
|
+
if (entry === undefined || entry.task.status !== "done") {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
const item = feedbackItemFromEntry(entry);
|
|
234
|
+
const finalNote = userNoteCandidateFromEntry(entry);
|
|
235
|
+
const generatedFiles = generatedFilesForCompletedEntries(input, [entry]);
|
|
236
|
+
const result = {
|
|
237
|
+
kind: "task_result",
|
|
238
|
+
status: "completed",
|
|
239
|
+
title: "Task completed",
|
|
240
|
+
task_id: item.task_id,
|
|
241
|
+
task_title: item.title,
|
|
242
|
+
result_key: taskResultKey(item),
|
|
243
|
+
summary: item.summary,
|
|
244
|
+
...(item.module_name === undefined ? {} : { module_name: item.module_name }),
|
|
245
|
+
...(item.latest_run_result_id === undefined
|
|
246
|
+
? {}
|
|
247
|
+
: { latest_run_result_id: item.latest_run_result_id }),
|
|
248
|
+
...(input.activityRef === undefined ? {} : { activity_ref: input.activityRef }),
|
|
249
|
+
...(finalNote === undefined ? {} : { final_note: finalNote }),
|
|
250
|
+
...(generatedFiles.length === 0 ? {} : { generated_files: generatedFiles }),
|
|
251
|
+
};
|
|
252
|
+
return publishTaskResultMessage(input, result);
|
|
253
|
+
}
|
|
254
|
+
export function emitTaskFailedResultMessage(input) {
|
|
255
|
+
const entry = readWorklistTaskEntries(input.store).find((item) => item.task.id === input.taskId);
|
|
256
|
+
if (entry === undefined || entry.task.status !== "failed") {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
const item = feedbackItemFromEntry(entry);
|
|
260
|
+
const errorCode = entry.detail?.detail.result.error_code;
|
|
261
|
+
const result = {
|
|
262
|
+
kind: "task_result",
|
|
263
|
+
status: "failed",
|
|
264
|
+
title: "Task failed",
|
|
265
|
+
task_id: item.task_id,
|
|
266
|
+
task_title: item.title,
|
|
267
|
+
result_key: taskResultKey(item),
|
|
268
|
+
summary: item.summary,
|
|
269
|
+
...(item.module_name === undefined ? {} : { module_name: item.module_name }),
|
|
270
|
+
...(item.latest_run_result_id === undefined
|
|
271
|
+
? {}
|
|
272
|
+
: { latest_run_result_id: item.latest_run_result_id }),
|
|
273
|
+
...(input.activityRef === undefined ? {} : { activity_ref: input.activityRef }),
|
|
274
|
+
...(errorCode === undefined ? {} : { error_code: errorCode }),
|
|
275
|
+
};
|
|
276
|
+
return publishTaskResultMessage(input, result);
|
|
277
|
+
}
|
|
180
278
|
function publishFeedbackMessage(input, feedback) {
|
|
181
279
|
if (hasExistingFeedback(input.store, feedback.feedback_key)) {
|
|
182
280
|
return null;
|
|
@@ -15,12 +15,21 @@ export type CliCommand = {
|
|
|
15
15
|
} | {
|
|
16
16
|
kind: "ps";
|
|
17
17
|
manage: boolean;
|
|
18
|
+
all: boolean;
|
|
18
19
|
} | {
|
|
19
20
|
kind: "stop";
|
|
20
21
|
target?: string;
|
|
21
22
|
} | {
|
|
22
23
|
kind: "invite";
|
|
23
24
|
target?: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: "archive";
|
|
27
|
+
target: string;
|
|
28
|
+
yes: boolean;
|
|
29
|
+
} | {
|
|
30
|
+
kind: "forget";
|
|
31
|
+
target: string;
|
|
32
|
+
yes: boolean;
|
|
24
33
|
} | {
|
|
25
34
|
kind: "logs";
|
|
26
35
|
target?: string;
|
|
@@ -49,10 +49,7 @@ function readOptionalTarget(args) {
|
|
|
49
49
|
export function parseCliArgs(argv, cwd = process.cwd()) {
|
|
50
50
|
const args = [...argv];
|
|
51
51
|
const command = args.shift();
|
|
52
|
-
if (command === undefined ||
|
|
53
|
-
command === "--help" ||
|
|
54
|
-
command === "-h" ||
|
|
55
|
-
command === "help") {
|
|
52
|
+
if (command === undefined || command === "--help" || command === "-h" || command === "help") {
|
|
56
53
|
return { kind: "help", ...(args[0] === undefined ? {} : { topic: args[0] }) };
|
|
57
54
|
}
|
|
58
55
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
@@ -115,17 +112,22 @@ export function parseCliArgs(argv, cwd = process.cwd()) {
|
|
|
115
112
|
}
|
|
116
113
|
if (command === "ps") {
|
|
117
114
|
let manage = false;
|
|
115
|
+
let all = false;
|
|
118
116
|
for (const arg of args) {
|
|
119
117
|
if (arg === "--manage") {
|
|
120
118
|
manage = true;
|
|
121
119
|
continue;
|
|
122
120
|
}
|
|
121
|
+
if (arg === "--all") {
|
|
122
|
+
all = true;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
123
125
|
if (isHelpArg(arg)) {
|
|
124
126
|
return { kind: "help", topic: "ps" };
|
|
125
127
|
}
|
|
126
128
|
throw new LaunchError("git_bootstrap_failed", `Unsupported option: ${arg}`, "Use `tutti ps --help` to see supported ps options.");
|
|
127
129
|
}
|
|
128
|
-
return { kind: "ps", manage };
|
|
130
|
+
return { kind: "ps", manage, all };
|
|
129
131
|
}
|
|
130
132
|
if (command === "stop") {
|
|
131
133
|
if (hasHelpArg(args)) {
|
|
@@ -141,6 +143,54 @@ export function parseCliArgs(argv, cwd = process.cwd()) {
|
|
|
141
143
|
const target = readOptionalTarget(args);
|
|
142
144
|
return { kind: "invite", ...(target === undefined ? {} : { target }) };
|
|
143
145
|
}
|
|
146
|
+
if (command === "archive") {
|
|
147
|
+
if (hasHelpArg(args)) {
|
|
148
|
+
return { kind: "help", topic: "archive" };
|
|
149
|
+
}
|
|
150
|
+
let target;
|
|
151
|
+
let yes = false;
|
|
152
|
+
for (const arg of args) {
|
|
153
|
+
if (arg === "--yes") {
|
|
154
|
+
yes = true;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (arg.startsWith("--")) {
|
|
158
|
+
throw new LaunchError("git_bootstrap_failed", `Unsupported option: ${arg}`, "Run `tutti archive --help` to see supported archive options.");
|
|
159
|
+
}
|
|
160
|
+
if (target !== undefined) {
|
|
161
|
+
throw new LaunchError("git_bootstrap_failed", "Only one archive target is supported", "Run `tutti archive <target> [--yes]`.");
|
|
162
|
+
}
|
|
163
|
+
target = arg;
|
|
164
|
+
}
|
|
165
|
+
if (target === undefined) {
|
|
166
|
+
throw new LaunchError("project_target_not_found", "Archive requires a project target", "Run `tutti archive <project-id> [--yes]`.");
|
|
167
|
+
}
|
|
168
|
+
return { kind: "archive", target, yes };
|
|
169
|
+
}
|
|
170
|
+
if (command === "forget") {
|
|
171
|
+
if (hasHelpArg(args)) {
|
|
172
|
+
return { kind: "help", topic: "forget" };
|
|
173
|
+
}
|
|
174
|
+
let target;
|
|
175
|
+
let yes = false;
|
|
176
|
+
for (const arg of args) {
|
|
177
|
+
if (arg === "--yes") {
|
|
178
|
+
yes = true;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (arg.startsWith("--")) {
|
|
182
|
+
throw new LaunchError("git_bootstrap_failed", `Unsupported option: ${arg}`, "Run `tutti forget --help` to see supported forget options.");
|
|
183
|
+
}
|
|
184
|
+
if (target !== undefined) {
|
|
185
|
+
throw new LaunchError("git_bootstrap_failed", "Only one forget target is supported", "Run `tutti forget <target> [--yes]`.");
|
|
186
|
+
}
|
|
187
|
+
target = arg;
|
|
188
|
+
}
|
|
189
|
+
if (target === undefined) {
|
|
190
|
+
throw new LaunchError("project_target_not_found", "Forget requires a project target", "Run `tutti forget <project-id> [--yes]`.");
|
|
191
|
+
}
|
|
192
|
+
return { kind: "forget", target, yes };
|
|
193
|
+
}
|
|
144
194
|
if (command === "logs") {
|
|
145
195
|
if (hasHelpArg(args)) {
|
|
146
196
|
return { kind: "help", topic: "logs" };
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { formatCliError } from "./errors.js";
|
|
3
3
|
import { parseCliArgs } from "./args.js";
|
|
4
|
-
import { runBackgroundLaunchCommand, runForegroundLaunchCommand, runInternalHostRunCommand, } from "./launch-command.js";
|
|
4
|
+
import { confirmTextFromTty, 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 { runDoctorCommand, runInviteCommand, runLogsCommand, runProviderStatusCommand, runPsManageCommand, runPsCommand, runStopCommand, } from "./runtime-commands.js";
|
|
7
|
+
import { runArchiveCommand, runDoctorCommand, runForgetCommand, 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]
|
|
11
|
-
tutti ps [--manage]
|
|
11
|
+
tutti ps [--manage] [--all]
|
|
12
12
|
tutti invite [target]
|
|
13
13
|
tutti stop [target]
|
|
14
|
+
tutti archive <target> [--yes]
|
|
15
|
+
tutti forget <target> [--yes]
|
|
14
16
|
tutti logs [target] [--tail 120]
|
|
15
17
|
tutti provider setup [target]
|
|
16
18
|
tutti provider status [target]
|
|
@@ -42,10 +44,11 @@ Commands:
|
|
|
42
44
|
status Show the redacted provider status for a project.
|
|
43
45
|
`;
|
|
44
46
|
const PS_HELP_TEXT = `Usage:
|
|
45
|
-
tutti ps [--manage]
|
|
47
|
+
tutti ps [--manage] [--all]
|
|
46
48
|
|
|
47
49
|
Options:
|
|
48
50
|
--manage Open the keyboard project manager.
|
|
51
|
+
--all Include stopped and binding-only machine projects.
|
|
49
52
|
`;
|
|
50
53
|
const INVITE_HELP_TEXT = `Usage:
|
|
51
54
|
tutti invite [target]
|
|
@@ -57,6 +60,24 @@ const STOP_HELP_TEXT = `Usage:
|
|
|
57
60
|
|
|
58
61
|
Stops the background Tutti host for a project.
|
|
59
62
|
`;
|
|
63
|
+
const ARCHIVE_HELP_TEXT = `Usage:
|
|
64
|
+
tutti archive <target> [--yes]
|
|
65
|
+
|
|
66
|
+
Archives the Relay project for all members after safely stopping its host.
|
|
67
|
+
Memberships and machine-local project state are retained.
|
|
68
|
+
|
|
69
|
+
Options:
|
|
70
|
+
--yes Confirm global Relay archive in a non-interactive terminal.
|
|
71
|
+
`;
|
|
72
|
+
const FORGET_HELP_TEXT = `Usage:
|
|
73
|
+
tutti forget <target> [--yes]
|
|
74
|
+
|
|
75
|
+
Deletes this project's machine-local state after safely stopping its host.
|
|
76
|
+
It does not archive the Relay project or change other members.
|
|
77
|
+
|
|
78
|
+
Options:
|
|
79
|
+
--yes Confirm destructive local deletion in a non-interactive terminal.
|
|
80
|
+
`;
|
|
60
81
|
const LOGS_HELP_TEXT = `Usage:
|
|
61
82
|
tutti logs [target] [--tail 120]
|
|
62
83
|
|
|
@@ -83,6 +104,12 @@ function helpForTopic(topic) {
|
|
|
83
104
|
if (topic === "stop") {
|
|
84
105
|
return STOP_HELP_TEXT;
|
|
85
106
|
}
|
|
107
|
+
if (topic === "archive") {
|
|
108
|
+
return ARCHIVE_HELP_TEXT;
|
|
109
|
+
}
|
|
110
|
+
if (topic === "forget") {
|
|
111
|
+
return FORGET_HELP_TEXT;
|
|
112
|
+
}
|
|
86
113
|
if (topic === "logs") {
|
|
87
114
|
return LOGS_HELP_TEXT;
|
|
88
115
|
}
|
|
@@ -147,15 +174,27 @@ try {
|
|
|
147
174
|
break;
|
|
148
175
|
case "ps":
|
|
149
176
|
if (command.manage) {
|
|
150
|
-
await runPsManageCommand();
|
|
177
|
+
await runPsManageCommand({ all: command.all });
|
|
151
178
|
}
|
|
152
179
|
else {
|
|
153
|
-
process.stdout.write(`${await runPsCommand()}\n`);
|
|
180
|
+
process.stdout.write(`${await runPsCommand({ all: command.all })}\n`);
|
|
154
181
|
}
|
|
155
182
|
break;
|
|
156
183
|
case "stop":
|
|
157
184
|
process.stdout.write(`${await runStopCommand(command.target)}\n`);
|
|
158
185
|
break;
|
|
186
|
+
case "archive":
|
|
187
|
+
process.stdout.write(`${await runArchiveCommand(command.target, {
|
|
188
|
+
yes: command.yes,
|
|
189
|
+
confirm: confirmTextFromTty,
|
|
190
|
+
})}\n`);
|
|
191
|
+
break;
|
|
192
|
+
case "forget":
|
|
193
|
+
process.stdout.write(`${await runForgetCommand(command.target, {
|
|
194
|
+
yes: command.yes,
|
|
195
|
+
confirm: confirmTextFromTty,
|
|
196
|
+
})}\n`);
|
|
197
|
+
break;
|
|
159
198
|
case "invite":
|
|
160
199
|
process.stdout.write(`${await runInviteCommand(command.target)}\n`);
|
|
161
200
|
break;
|
|
@@ -169,7 +208,7 @@ try {
|
|
|
169
208
|
process.stdout.write(`${runProviderStatusCommand(command.target)}\n`);
|
|
170
209
|
break;
|
|
171
210
|
case "doctor":
|
|
172
|
-
process.stdout.write(`${runDoctorCommand(command.target)}\n`);
|
|
211
|
+
process.stdout.write(`${await runDoctorCommand(command.target)}\n`);
|
|
173
212
|
break;
|
|
174
213
|
}
|
|
175
214
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type LaunchErrorCode = "unsafe_project_root" | "unsupported_tutti_schema" | "project_identity_conflict" | "git_bootstrap_failed" | "sensitive_file_detected" | "relay_unavailable" | "relay_registration_failed" | "host_not_running" | "project_target_ambiguous" | "project_target_not_found" | "provider_configuration_required" | "provider_setup_cancelled" | "provider_validation_failed" | "store_migration_failed" | "existing_server_unhealthy" | "takeover_rejected" | "takeover_failed" | "port_unavailable";
|
|
1
|
+
export type LaunchErrorCode = "unsafe_project_root" | "unsupported_tutti_schema" | "project_identity_conflict" | "git_bootstrap_failed" | "sensitive_file_detected" | "relay_unavailable" | "relay_registration_failed" | "host_not_running" | "archive_confirmation_required" | "archive_rejected" | "forget_confirmation_required" | "forget_rejected" | "project_target_ambiguous" | "project_target_not_found" | "provider_configuration_required" | "provider_setup_cancelled" | "provider_validation_failed" | "store_migration_failed" | "existing_server_unhealthy" | "takeover_rejected" | "takeover_failed" | "port_unavailable";
|
|
2
2
|
export declare class LaunchError extends Error {
|
|
3
3
|
readonly code: LaunchErrorCode;
|
|
4
4
|
readonly next: string;
|
|
@@ -44,7 +44,12 @@ export function formatCliError(error) {
|
|
|
44
44
|
if (error.code === "provider_setup_cancelled") {
|
|
45
45
|
return ["Tutti command cancelled", "", `Reason: ${redactText(error.message)}`].join("\n");
|
|
46
46
|
}
|
|
47
|
-
const lines = [
|
|
47
|
+
const lines = [
|
|
48
|
+
"Tutti command failed",
|
|
49
|
+
"",
|
|
50
|
+
`Code: ${error.code}`,
|
|
51
|
+
`Reason: ${formatBoundedText(error.message)}`,
|
|
52
|
+
];
|
|
48
53
|
const detailLines = Object.entries(error.details)
|
|
49
54
|
.filter(([, value]) => value !== undefined)
|
|
50
55
|
.map(([key, value]) => `Detail ${key}: ${formatBoundedDetail(value)}`);
|
|
@@ -37,6 +37,7 @@ export type StartLaunchProjectOptions = LaunchPreparationOptions & {
|
|
|
37
37
|
connectRelayHostTunnel?: RelayHostTunnelConnector;
|
|
38
38
|
probeRuntimeEndpoint?: HostRuntimeEndpointProbe;
|
|
39
39
|
registerRelayHostConnection?: RelayHostConnectionRegistrar;
|
|
40
|
+
reconnectWaitMs?: number;
|
|
40
41
|
shutdownWaitMs?: number;
|
|
41
42
|
};
|
|
42
43
|
export type FormatLaunchLifecycleResultOptions = {
|