@xfey/tutti 0.1.42 → 0.1.44

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 (55) hide show
  1. package/dist/artifacts/candidate-validator.d.ts +39 -0
  2. package/dist/artifacts/candidate-validator.js +512 -0
  3. package/dist/artifacts/index.d.ts +3 -1
  4. package/dist/artifacts/index.js +3 -1
  5. package/dist/artifacts/manifest-contract.d.ts +30 -0
  6. package/dist/artifacts/manifest-contract.js +214 -0
  7. package/dist/artifacts/manifest.d.ts +3 -12
  8. package/dist/artifacts/manifest.js +29 -201
  9. package/dist/artifacts/preview-runtime.d.ts +29 -10
  10. package/dist/artifacts/preview-runtime.js +400 -155
  11. package/dist/artifacts/process-boundary.d.ts +33 -0
  12. package/dist/artifacts/process-boundary.js +134 -0
  13. package/dist/control-plane/types.d.ts +7 -2
  14. package/dist/prompt-templates/index.d.ts +1 -1
  15. package/dist/prompt-templates/index.js +1 -0
  16. package/dist/provider-usage/index.js +1 -0
  17. package/dist/run-pipeline/artifact-applicability.d.ts +49 -0
  18. package/dist/run-pipeline/artifact-applicability.js +281 -0
  19. package/dist/run-pipeline/openai.d.ts +6 -0
  20. package/dist/run-pipeline/openai.js +72 -2
  21. package/dist/run-pipeline/promotion-reconcile.d.ts +9 -1
  22. package/dist/run-pipeline/promotion-reconcile.js +65 -11
  23. package/dist/server-shell/cli/launch.d.ts +2 -0
  24. package/dist/server-shell/cli/launch.js +7 -1
  25. package/dist/server-shell/http/host-tunnel.js +1 -1
  26. package/dist/server-shell/http/routes/project-api/artifacts-routes.js +63 -14
  27. package/dist/server-shell/http/routes/project-api/types.d.ts +2 -0
  28. package/dist/server-shell/local-console/project-service.d.ts +5 -0
  29. package/dist/server-shell/local-console/project-service.js +25 -0
  30. package/dist/server-shell/local-console/server.js +10 -0
  31. package/dist/workspace-ops/index.d.ts +1 -1
  32. package/dist/workspace-ops/index.js +1 -1
  33. package/dist/workspace-ops/run-workspaces.d.ts +1 -0
  34. package/dist/workspace-ops/run-workspaces.js +27 -0
  35. package/node_modules/@tutti/shared/dist/schemas/api/artifacts.d.ts +28 -8
  36. package/node_modules/@tutti/shared/dist/schemas/api/artifacts.js +34 -9
  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/types.d.ts +5 -1
  40. package/node_modules/@tutti/shared/dist/utils/redaction/index.d.ts +1 -1
  41. package/node_modules/@tutti/shared/dist/utils/redaction/index.js +14 -0
  42. package/package.json +1 -1
  43. package/prompts/README.md +1 -0
  44. package/prompts/artifacts/README.md +7 -0
  45. package/prompts/artifacts/applicability.md +41 -0
  46. package/prompts/prompt-flow-map.md +29 -26
  47. package/prompts/runs/README.md +3 -3
  48. package/prompts/runs/task-continuation.md +36 -4
  49. package/prompts/runs/task-retry.md +42 -6
  50. package/prompts/runs/task-run.md +36 -4
  51. package/web/assets/index-B7I1QwT_.js +69 -0
  52. package/web/assets/index-F-ouhuJ5.css +1 -0
  53. package/web/index.html +2 -2
  54. package/web/assets/index-Sqw_t67u.css +0 -1
  55. package/web/assets/index-cYUjFgtc.js +0 -29
@@ -1,7 +1,15 @@
1
+ import type { CandidateArtifactValidationFailure } from "../artifacts/index.js";
1
2
  import type { RunPipelineInput, RunPipelineResult } from "../control-plane/index.js";
2
3
  import { type ResultCommit, type RunWorkspaceHandle } from "../workspace-ops/index.js";
3
4
  import type { OpenAiRunPipelineRunnerOptions } from "./openai.js";
4
5
  import type { CompletedNoRepoChangesTaskRunOutput, CompletedTaskRunOutput } from "./task-run-output.js";
6
+ export type PromotionReconcileOutcome = {
7
+ result: RunPipelineResult;
8
+ artifact_gate_failure?: {
9
+ correctable: boolean;
10
+ failure: CandidateArtifactValidationFailure;
11
+ };
12
+ };
5
13
  export declare function runPromotionReconcile(input: {
6
14
  options: OpenAiRunPipelineRunnerOptions;
7
15
  pipelineInput: RunPipelineInput;
@@ -9,5 +17,5 @@ export declare function runPromotionReconcile(input: {
9
17
  taskOutput: CompletedTaskRunOutput | CompletedNoRepoChangesTaskRunOutput;
10
18
  sourceCommit: ResultCommit;
11
19
  changedPaths: string[];
12
- }): Promise<RunPipelineResult>;
20
+ }): Promise<PromotionReconcileOutcome>;
13
21
  //# sourceMappingURL=promotion-reconcile.d.ts.map
@@ -2,7 +2,14 @@ import { dirname, join } from "node:path";
2
2
  import { createNodeNpmCheckPlan, executeCheckPlan } from "../checks/index.js";
3
3
  import { applyRunResultToMergeWorkspace, cleanupMergeWorkspace, collectMergeDiff, createMergeCandidateCommit, prepareMergeWorkspace, promoteMergeCandidate, WorkspaceOpsError, } from "../workspace-ops/index.js";
4
4
  import { candidateChangedAfterChecks } from "./candidate-diff.js";
5
+ import { runArtifactCandidateGate } from "./artifact-applicability.js";
5
6
  import { checkSkipFields, checkStatus, checksSummary, docsStatusForChangedPaths, failedBeforeChecks, failedFinishedResult, promotionNotPromoted, successfulRunSummary, } from "./result-projections.js";
7
+ function reconcileOutcome(result, artifactGateFailure) {
8
+ return {
9
+ result,
10
+ ...(artifactGateFailure === undefined ? {} : { artifact_gate_failure: artifactGateFailure }),
11
+ };
12
+ }
6
13
  function withPromotionReconcileEvent(result, event) {
7
14
  return {
8
15
  ...result,
@@ -40,13 +47,60 @@ export async function runPromotionReconcile(input) {
40
47
  changedPaths = diff.changed_paths;
41
48
  if (changedPaths.length === 0) {
42
49
  const summary = "Promotion reconcile produced no candidate changes.";
43
- return withPromotionReconcileEvent(failedBeforeChecks({
50
+ return reconcileOutcome(withPromotionReconcileEvent(failedBeforeChecks({
44
51
  summary,
45
52
  errorCode: "promotion_reconcile_no_candidate_changes",
46
53
  retryable: false,
47
54
  changedPaths,
48
55
  promotion: promotionNotPromoted("promotion_reconcile_no_candidate_changes", "No reconciled candidate changes were available for promotion."),
56
+ }), failedPromotionReconcileEvent({ summary, changedPaths })));
57
+ }
58
+ input.pipelineInput.on_stage?.({
59
+ stage: "artifact_candidate_validate",
60
+ summary: "validating the reconciled candidate Artifact preview.",
61
+ });
62
+ const artifactGate = await runArtifactCandidateGate({
63
+ repoRoot: handle.repo_root,
64
+ task: {
65
+ title: input.pipelineInput.task.title,
66
+ goal: input.pipelineInput.task_detail.detail.contract.goal,
67
+ scope: input.pipelineInput.task_detail.detail.contract.scope,
68
+ },
69
+ diff,
70
+ config: input.options.config,
71
+ apiKey: input.options.apiKey,
72
+ ...(input.options.promptsRoot === undefined
73
+ ? {}
74
+ : { promptsRoot: input.options.promptsRoot }),
75
+ ...(input.options.baseEnv === undefined ? {} : { baseEnv: input.options.baseEnv }),
76
+ ...(input.options.artifactApplicabilityClient === undefined
77
+ ? {}
78
+ : { client: input.options.artifactApplicabilityClient }),
79
+ ...(input.options.artifactApplicabilityJudge === undefined
80
+ ? {}
81
+ : { judge: input.options.artifactApplicabilityJudge }),
82
+ ...(input.options.validateArtifactCandidate === undefined
83
+ ? {}
84
+ : { validateCandidate: input.options.validateArtifactCandidate }),
85
+ ...(input.options.recordUsage === undefined
86
+ ? {}
87
+ : { recordUsage: input.options.recordUsage }),
88
+ activityRef: input.pipelineInput.activity_ref,
89
+ taskId: input.pipelineInput.task.id,
90
+ });
91
+ if (artifactGate.status === "failed") {
92
+ const summary = artifactGate.failure.summary;
93
+ const result = withPromotionReconcileEvent(failedBeforeChecks({
94
+ summary,
95
+ errorCode: artifactGate.failure.reason_code,
96
+ retryable: false,
97
+ changedPaths,
98
+ promotion: promotionNotPromoted(artifactGate.failure.reason_code, "Reconciled Artifact candidate validation failed, so promotion failed."),
49
99
  }), failedPromotionReconcileEvent({ summary, changedPaths }));
100
+ return reconcileOutcome(result, {
101
+ correctable: artifactGate.correctable,
102
+ failure: artifactGate.failure,
103
+ });
50
104
  }
51
105
  let docs = docsStatusForChangedPaths(changedPaths);
52
106
  const checkPlan = createNodeNpmCheckPlan({ workspaceRoot: handle.repo_root });
@@ -59,20 +113,20 @@ export async function runPromotionReconcile(input) {
59
113
  });
60
114
  if (checks.status === "failed") {
61
115
  const summary = checksSummary(checks);
62
- return withPromotionReconcileEvent(failedFinishedResult({
116
+ return reconcileOutcome(withPromotionReconcileEvent(failedFinishedResult({
63
117
  summary,
64
118
  checks,
65
119
  docs,
66
120
  changedPaths,
67
121
  promotion: promotionNotPromoted("promotion_reconcile_checks_failed", "Reconciled candidate checks failed, so promotion failed."),
68
- }), failedPromotionReconcileEvent({ summary, changedPaths }));
122
+ }), failedPromotionReconcileEvent({ summary, changedPaths })));
69
123
  }
70
124
  const postChecksDiff = collectMergeDiff(handle);
71
125
  changedPaths = postChecksDiff.changed_paths;
72
126
  docs = docsStatusForChangedPaths(changedPaths);
73
127
  if (candidateChangedAfterChecks(diff, postChecksDiff)) {
74
128
  const summary = "Checks modified the reconciled candidate workspace; promotion failed.";
75
- return withPromotionReconcileEvent(failedFinishedResult({
129
+ return reconcileOutcome(withPromotionReconcileEvent(failedFinishedResult({
76
130
  summary,
77
131
  checks: {
78
132
  status: "failed",
@@ -81,11 +135,11 @@ export async function runPromotionReconcile(input) {
81
135
  docs,
82
136
  changedPaths,
83
137
  promotion: promotionNotPromoted("promotion_reconcile_checks_modified_candidate", "Checks changed tracked reconciled candidate files, so promotion failed."),
84
- }), failedPromotionReconcileEvent({ summary, changedPaths }));
138
+ }), failedPromotionReconcileEvent({ summary, changedPaths })));
85
139
  }
86
140
  const commit = createMergeCandidateCommit(handle, `Reconcile Tutti task: ${input.pipelineInput.task.title}`);
87
141
  const promotion = promoteMergeCandidate(handle, commit);
88
- return withPromotionReconcileEvent({
142
+ return reconcileOutcome(withPromotionReconcileEvent({
89
143
  result_kind: "finished",
90
144
  outcome: "completed",
91
145
  summary: successfulRunSummary({
@@ -111,26 +165,26 @@ export async function runPromotionReconcile(input) {
111
165
  summary: "Promotion reconciled and promoted.",
112
166
  commit_oid: promotion.promoted_commit_oid,
113
167
  meta: promotionReconcileMeta(changedPaths),
114
- });
168
+ }));
115
169
  }
116
170
  catch (error) {
117
171
  if (error instanceof WorkspaceOpsError) {
118
- return withPromotionReconcileEvent(failedBeforeChecks({
172
+ return reconcileOutcome(withPromotionReconcileEvent(failedBeforeChecks({
119
173
  summary: error.message,
120
174
  errorCode: `workspace_ops_${error.code}`,
121
175
  retryable: error.code === "conflict",
122
176
  changedPaths,
123
177
  promotion: promotionNotPromoted("promotion_reconcile_failed", error.message),
124
- }), failedPromotionReconcileEvent({ summary: error.message, changedPaths }));
178
+ }), failedPromotionReconcileEvent({ summary: error.message, changedPaths })));
125
179
  }
126
180
  const summary = error instanceof Error ? error.message : "Promotion reconcile failed";
127
- return withPromotionReconcileEvent(failedBeforeChecks({
181
+ return reconcileOutcome(withPromotionReconcileEvent(failedBeforeChecks({
128
182
  summary,
129
183
  errorCode: "promotion_reconcile_failed",
130
184
  retryable: false,
131
185
  changedPaths,
132
186
  promotion: promotionNotPromoted("promotion_reconcile_failed", summary),
133
- }), failedPromotionReconcileEvent({ summary, changedPaths }));
187
+ }), failedPromotionReconcileEvent({ summary, changedPaths })));
134
188
  }
135
189
  finally {
136
190
  if (handle !== undefined) {
@@ -11,6 +11,8 @@ export type LaunchPreparationOptions = {
11
11
  now?: () => Date;
12
12
  confirm?: LaunchConfirmationHandler;
13
13
  allowWorkspaceRootTakeover?: boolean;
14
+ projectDisplayName?: string;
15
+ projectDescription?: string;
14
16
  };
15
17
  export type LaunchProviderStatus = OpenAiProviderConfigProjection["status"];
16
18
  export type LaunchPreparationResult = {
@@ -4,7 +4,7 @@ import { readOpenAiProviderConfigProjection, resolveTuttiHome, } from "../../pro
4
4
  import { LaunchError } from "./errors.js";
5
5
  import { assertSafeProjectRoot, ensureGitBootstrap, } from "./git-bootstrap.js";
6
6
  import { ensureMachineProjectBinding } from "./machine-local.js";
7
- import { readProjectIdentity, readProjectDisplayName, writeProjectIdentity, } from "./project-identity.js";
7
+ import { readProjectIdentity, readProjectDisplayName, writeProjectDescription, writeProjectDisplayName, writeProjectIdentity, } from "./project-identity.js";
8
8
  const DEFAULT_RELAY_URL = "https://tutti.now";
9
9
  const DEFAULT_HOST_ENDPOINT_HOST = "127.0.0.1";
10
10
  const AUTO_HOST_ENDPOINT_PORT = 0;
@@ -64,6 +64,12 @@ export async function prepareLaunchProject(options) {
64
64
  gitOptions.confirm = options.confirm;
65
65
  }
66
66
  const git = await ensureGitBootstrap(gitOptions);
67
+ if (projectIdentityCreated && options.projectDisplayName !== undefined) {
68
+ writeProjectDisplayName(workspaceRoot, options.projectDisplayName);
69
+ }
70
+ if (projectIdentityCreated && options.projectDescription !== undefined) {
71
+ writeProjectDescription(workspaceRoot, options.projectDescription);
72
+ }
67
73
  const bindingOptions = {
68
74
  tuttiHome,
69
75
  projectId,
@@ -64,7 +64,7 @@ function toInjectMethod(method) {
64
64
  case "OPTIONS":
65
65
  return normalized;
66
66
  default:
67
- return "GET";
67
+ return "TRACE";
68
68
  }
69
69
  }
70
70
  export function createHostProjectApiTunnelRequestHandler(options) {
@@ -1,13 +1,46 @@
1
1
  import { HeartbeatArtifactPreviewBodySchema, HeartbeatArtifactPreviewResponseSchema, GetArtifactsResponseSchema, StartArtifactPreviewBodySchema, StartArtifactPreviewResponseSchema, StopArtifactPreviewBodySchema, StopArtifactPreviewResponseSchema, } from "@tutti/shared/schemas/api";
2
- import { ARTIFACT_PREVIEW_CSP, ArtifactPreviewManager, readArtifactProjection, readArtifactStaticPreview, } from "../../../../artifacts/index.js";
2
+ import { ARTIFACT_PREVIEW_REQUEST_BODY_LIMIT_BYTES, ARTIFACT_PREVIEW_CSP, ArtifactPreviewManager, isArtifactPreviewHttpMethod, readArtifactProjection, readArtifactStaticPreview, } from "../../../../artifacts/index.js";
3
+ import { WorkspaceOpsError } from "../../../../workspace-ops/index.js";
3
4
  import { executeIdempotentCommand } from "../../../command-idempotency/index.js";
4
5
  import { HOST_PROJECT_API_BASE_PATH } from "./constants.js";
5
6
  import { requireHostProjectSession, requireProjectStore, requireWorkspaceRoot, } from "./session-requirements.js";
6
7
  function artifactInvalidates() {
7
8
  return [{ kind: "artifacts" }];
8
9
  }
10
+ const ARTIFACT_PREVIEW_ALLOWED_METHODS = "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS";
11
+ function previewRequestHeaders(request) {
12
+ const headers = {};
13
+ for (const [name, value] of Object.entries(request.headers)) {
14
+ if (typeof value === "string") {
15
+ headers[name] = value;
16
+ }
17
+ }
18
+ return headers;
19
+ }
20
+ function previewRequestBody(body) {
21
+ if (body === undefined || body === null) {
22
+ return undefined;
23
+ }
24
+ if (Buffer.isBuffer(body)) {
25
+ return body;
26
+ }
27
+ if (body instanceof Uint8Array) {
28
+ return Buffer.from(body);
29
+ }
30
+ if (typeof body === "string") {
31
+ return Buffer.from(body, "utf8");
32
+ }
33
+ return Buffer.from(JSON.stringify(body), "utf8");
34
+ }
35
+ function requireCurrentArtifactManifestMarker(request, manifestBlobOid) {
36
+ const marker = new URL(request.url, "http://artifact.invalid").searchParams.get("v");
37
+ if (marker !== null && marker !== manifestBlobOid) {
38
+ throw new WorkspaceOpsError("not_found", "Artifact preview manifest is stale");
39
+ }
40
+ }
9
41
  export function registerArtifactsRoutes(app, { options, workspaceEvents, }) {
10
42
  const artifactPreviewManager = new ArtifactPreviewManager({
43
+ ...options.artifactPreview,
11
44
  ...(options.now === undefined ? {} : { now: options.now }),
12
45
  onStatusChange: () => {
13
46
  workspaceEvents.publish({
@@ -20,9 +53,8 @@ export function registerArtifactsRoutes(app, { options, workspaceEvents, }) {
20
53
  });
21
54
  },
22
55
  });
23
- app.addHook("onClose", (_instance, done) => {
24
- artifactPreviewManager.stopAll();
25
- done();
56
+ app.addHook("onClose", async () => {
57
+ await artifactPreviewManager.stopAll();
26
58
  });
27
59
  app.get(`${HOST_PROJECT_API_BASE_PATH}/artifacts`, {
28
60
  schema: {
@@ -36,27 +68,37 @@ export function registerArtifactsRoutes(app, { options, workspaceEvents, }) {
36
68
  workspaceRoot: requireWorkspaceRoot(options),
37
69
  });
38
70
  });
39
- app.get("/artifacts/preview/*", async (request, reply) => {
71
+ app.all("/artifacts/preview/*", {
72
+ bodyLimit: ARTIFACT_PREVIEW_REQUEST_BODY_LIMIT_BYTES,
73
+ }, async (request, reply) => {
40
74
  requireHostProjectSession(request, options);
75
+ const method = request.method.toUpperCase();
76
+ if (!isArtifactPreviewHttpMethod(method)) {
77
+ void reply
78
+ .code(405)
79
+ .header("allow", ARTIFACT_PREVIEW_ALLOWED_METHODS)
80
+ .send("Artifact preview method is not allowed");
81
+ return;
82
+ }
41
83
  const workspaceRoot = requireWorkspaceRoot(options);
42
84
  const projection = readArtifactProjection({
43
85
  workspaceRoot,
44
86
  ...(options.now === undefined ? {} : { now: options.now }),
45
87
  });
46
- if (projection.artifact.status === "declared" && projection.artifact.artifact.kind === "server") {
88
+ if (projection.artifact.status === "declared") {
89
+ requireCurrentArtifactManifestMarker(request, projection.artifact.manifest_blob_oid);
90
+ }
91
+ if (projection.artifact.status === "declared" &&
92
+ projection.artifact.artifact.kind === "server") {
47
93
  const queryIndex = request.url.indexOf("?");
94
+ const body = previewRequestBody(request.body);
48
95
  const preview = await artifactPreviewManager.fetchServerPreview({
49
96
  workspaceRoot,
50
97
  path: request.params["*"],
98
+ method,
51
99
  ...(queryIndex < 0 ? {} : { query: request.url.slice(queryIndex + 1) }),
52
- headers: {
53
- ...(typeof request.headers.accept === "string"
54
- ? { accept: request.headers.accept }
55
- : {}),
56
- ...(typeof request.headers["accept-language"] === "string"
57
- ? { "accept-language": request.headers["accept-language"] }
58
- : {}),
59
- },
100
+ headers: previewRequestHeaders(request),
101
+ ...(body === undefined ? {} : { body }),
60
102
  });
61
103
  for (const [key, value] of Object.entries(preview.headers)) {
62
104
  void reply.header(key, value);
@@ -69,6 +111,13 @@ export function registerArtifactsRoutes(app, { options, workspaceEvents, }) {
69
111
  .send(preview.content);
70
112
  return;
71
113
  }
114
+ if (method !== "GET" && method !== "HEAD") {
115
+ void reply
116
+ .code(405)
117
+ .header("allow", "GET, HEAD")
118
+ .send("Static Artifact previews only support GET and HEAD");
119
+ return;
120
+ }
72
121
  const preview = readArtifactStaticPreview({
73
122
  workspaceRoot,
74
123
  path: request.params["*"],
@@ -1,6 +1,7 @@
1
1
  import type { ReadOnlyChatAssistantHandle } from "../../../../chat-assistant/index.js";
2
2
  import type { Phase5ControlPlane } from "../../../../control-plane/index.js";
3
3
  import type { ApprovalRequestManager } from "../../../../approvals/index.js";
4
+ import type { ArtifactPreviewManagerOptions } from "../../../../artifacts/index.js";
4
5
  import type { HostProjectStore } from "../../../../store/index.js";
5
6
  import type { ProjectId, RelayProjectRef } from "@tutti/shared/ids";
6
7
  import type { AccountAvatarProjection } from "@tutti/shared/domain";
@@ -72,6 +73,7 @@ export type HostProjectApiCapabilities = {
72
73
  clarification: boolean;
73
74
  };
74
75
  export type HostProjectApiRouteOptions = {
76
+ artifactPreview?: Omit<ArtifactPreviewManagerOptions, "now" | "onStatusChange">;
75
77
  project: {
76
78
  project_id: ProjectId;
77
79
  display_name: string;
@@ -23,6 +23,10 @@ export type LocalConsoleProviderInput = {
23
23
  api_key: string;
24
24
  model: string;
25
25
  };
26
+ export type LocalConsoleProjectMetadataInput = {
27
+ display_name: string;
28
+ description?: string;
29
+ };
26
30
  export type LocalConsoleLaunchResult = {
27
31
  project: LocalConsoleProject;
28
32
  project_created: boolean;
@@ -59,6 +63,7 @@ export declare class LocalConsoleProjectService {
59
63
  }>;
60
64
  launchProject(input: {
61
65
  workspacePath: string;
66
+ project?: LocalConsoleProjectMetadataInput;
62
67
  provider?: LocalConsoleProviderInput;
63
68
  }, context: LocalConsoleInvocationContext): Promise<LocalConsoleLaunchResult>;
64
69
  openProject(projectId: string, context: LocalConsoleInvocationContext): Promise<LocalConsoleOpenResult>;
@@ -50,6 +50,20 @@ function validateProvider(input) {
50
50
  }
51
51
  return provider;
52
52
  }
53
+ function validateProjectMetadata(input) {
54
+ const displayName = input.display_name.trim();
55
+ const description = input.description?.trim();
56
+ if (displayName === "" || displayName.length > 80) {
57
+ throw new LocalConsoleProjectError("project_display_name_invalid", "Project name must be between 1 and 80 characters.");
58
+ }
59
+ if (description !== undefined && description.length > 160) {
60
+ throw new LocalConsoleProjectError("project_description_invalid", "Project description cannot exceed 160 characters.");
61
+ }
62
+ return {
63
+ display_name: displayName,
64
+ ...(description === undefined || description === "" ? {} : { description }),
65
+ };
66
+ }
53
67
  function projectFromRuntimeRow(row, providerStatus, providerModel, metadata) {
54
68
  const currentVersion = readCliVersion();
55
69
  const openUrl = projectOpenUrl(metadata.relayUrl, row.relay_project_ref);
@@ -220,6 +234,9 @@ export class LocalConsoleProjectService {
220
234
  }
221
235
  const launch = this.#runMutation(async () => await this.#launchProject({
222
236
  workspacePath,
237
+ ...(input.project === undefined
238
+ ? {}
239
+ : { project: validateProjectMetadata(input.project) }),
223
240
  ...(input.provider === undefined ? {} : { provider: validateProvider(input.provider) }),
224
241
  }, context)).finally(() => {
225
242
  this.#launches.delete(workspacePath);
@@ -234,6 +251,14 @@ export class LocalConsoleProjectService {
234
251
  preparation = await prepareLaunchProject({
235
252
  workspacePath: input.workspacePath,
236
253
  yes: true,
254
+ ...(input.project === undefined
255
+ ? {}
256
+ : {
257
+ projectDisplayName: input.project.display_name,
258
+ ...(input.project.description === undefined
259
+ ? {}
260
+ : { projectDescription: input.project.description }),
261
+ }),
237
262
  ...operation,
238
263
  });
239
264
  }
@@ -256,6 +256,15 @@ export function createLocalConsoleServer(options) {
256
256
  additionalProperties: false,
257
257
  properties: {
258
258
  workspace_path: { type: "string", minLength: 1, maxLength: 4096 },
259
+ project: {
260
+ type: "object",
261
+ required: ["display_name"],
262
+ additionalProperties: false,
263
+ properties: {
264
+ display_name: { type: "string", minLength: 1, maxLength: 80 },
265
+ description: { type: "string", maxLength: 160 },
266
+ },
267
+ },
259
268
  provider: {
260
269
  type: "object",
261
270
  required: ["base_url", "api_key", "model"],
@@ -273,6 +282,7 @@ export function createLocalConsoleServer(options) {
273
282
  const session = requireSession(request, true);
274
283
  return await projects.launchProject({
275
284
  workspacePath: request.body.workspace_path,
285
+ ...(request.body.project === undefined ? {} : { project: request.body.project }),
276
286
  ...(request.body.provider === undefined ? {} : { provider: request.body.provider }),
277
287
  }, session.context);
278
288
  });
@@ -4,7 +4,7 @@ export { initializeProjectDocs, readProjectDocsInitializationStatus, syncProject
4
4
  export { uploadReferenceFile } from "./reference-files.js";
5
5
  export { referenceFileCategoryForPath, referenceFileSourceForPath, referenceMediaTypeForPath, referenceTuttiGroupForPath, } from "./reference-metadata.js";
6
6
  export { readReferenceSummaryIndex, readReferenceSummaryTargets, referenceSummaryProjectionForEntry, updateReferenceSummaries, } from "./reference-summaries.js";
7
- export { applyRunResultToMergeWorkspace, cleanupMergeWorkspace, cleanupRunWorkspace, cleanupStaleRunWorkspaces, collectMergeDiff, collectRunDiff, createMergeCandidateCommit, createResultCommit, prepareMergeWorkspace, prepareRunWorkspace, promoteMergeCandidate, promoteRunResult, } from "./run-workspaces.js";
7
+ export { applyRunResultToMergeWorkspace, cleanupMergeWorkspace, cleanupRunWorkspace, cleanupStaleRunWorkspaces, collectMergeDiff, collectRunDiff, createMergeCandidateCommit, createResultCommit, fingerprintWorkspaceCandidate, prepareMergeWorkspace, prepareRunWorkspace, promoteMergeCandidate, promoteRunResult, } from "./run-workspaces.js";
8
8
  export { readViewerAsset, readViewerFile, readViewerSnapshot, readViewerTree } from "./viewer.js";
9
9
  export type { WorkspaceOpsErrorCode } from "./errors.js";
10
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";
@@ -4,6 +4,6 @@ export { initializeProjectDocs, readProjectDocsInitializationStatus, syncProject
4
4
  export { uploadReferenceFile } from "./reference-files.js";
5
5
  export { referenceFileCategoryForPath, referenceFileSourceForPath, referenceMediaTypeForPath, referenceTuttiGroupForPath, } from "./reference-metadata.js";
6
6
  export { readReferenceSummaryIndex, readReferenceSummaryTargets, referenceSummaryProjectionForEntry, updateReferenceSummaries, } from "./reference-summaries.js";
7
- export { applyRunResultToMergeWorkspace, cleanupMergeWorkspace, cleanupRunWorkspace, cleanupStaleRunWorkspaces, collectMergeDiff, collectRunDiff, createMergeCandidateCommit, createResultCommit, prepareMergeWorkspace, prepareRunWorkspace, promoteMergeCandidate, promoteRunResult, } from "./run-workspaces.js";
7
+ export { applyRunResultToMergeWorkspace, cleanupMergeWorkspace, cleanupRunWorkspace, cleanupStaleRunWorkspaces, collectMergeDiff, collectRunDiff, createMergeCandidateCommit, createResultCommit, fingerprintWorkspaceCandidate, prepareMergeWorkspace, prepareRunWorkspace, promoteMergeCandidate, promoteRunResult, } from "./run-workspaces.js";
8
8
  export { readViewerAsset, readViewerFile, readViewerSnapshot, readViewerTree } from "./viewer.js";
9
9
  //# sourceMappingURL=index.js.map
@@ -1,4 +1,5 @@
1
1
  import type { CleanupRunWorkspaceResult, CleanupStaleRunWorkspacesResult, MergeWorkspaceHandle, PrepareMergeWorkspaceOptions, PrepareRunWorkspaceOptions, PromotionResult, ResultCommit, RunDiffSummary, RunWorkspaceHandle } from "./types.js";
2
+ export declare function fingerprintWorkspaceCandidate(repoRoot: string): string;
2
3
  export declare function prepareRunWorkspace(options: PrepareRunWorkspaceOptions): RunWorkspaceHandle;
3
4
  export declare function collectRunDiff(handle: RunWorkspaceHandle): RunDiffSummary;
4
5
  export declare function createResultCommit(handle: RunWorkspaceHandle, summary: string): ResultCommit;
@@ -1,10 +1,37 @@
1
1
  import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
2
3
  import { join } from "node:path";
3
4
  import { classifyViewerPath } from "@tutti/shared/utils";
4
5
  import { WorkspaceOpsError } from "./errors.js";
5
6
  import { runGit, runGitRaw, runGitText, stagedChangesExist, summarizeGitFailure, tryGitText, } from "./git.js";
6
7
  import { requireMainlineWriteReady } from "./mainline.js";
7
8
  const MAX_RUN_DIFF_SUMMARY_BYTES = 32 * 1024;
9
+ export function fingerprintWorkspaceCandidate(repoRoot) {
10
+ const status = runGitRaw(["status", "--porcelain=v1", "-z"], repoRoot);
11
+ if (status.status !== 0) {
12
+ throw new WorkspaceOpsError("git_failed", summarizeGitFailure(["status"], status));
13
+ }
14
+ const diff = runGitRaw(["diff", "--binary", "HEAD", "--"], repoRoot);
15
+ if (diff.status !== 0) {
16
+ throw new WorkspaceOpsError("git_failed", summarizeGitFailure(["diff", "HEAD"], diff));
17
+ }
18
+ const untracked = runGitRaw(["ls-files", "--others", "--exclude-standard", "-z"], repoRoot);
19
+ if (untracked.status !== 0) {
20
+ throw new WorkspaceOpsError("git_failed", summarizeGitFailure(["ls-files", "--others"], untracked));
21
+ }
22
+ const hash = createHash("sha256").update(status.stdout).update(diff.stdout);
23
+ for (const path of untracked.stdout.toString("utf8").split("\0")) {
24
+ if (path.length === 0) {
25
+ continue;
26
+ }
27
+ const object = runGitRaw(["hash-object", "--no-filters", "--", path], repoRoot);
28
+ if (object.status !== 0) {
29
+ throw new WorkspaceOpsError("git_failed", summarizeGitFailure(["hash-object"], object));
30
+ }
31
+ hash.update(path).update("\0").update(object.stdout);
32
+ }
33
+ return hash.digest("hex");
34
+ }
8
35
  function runWorkspaceTempBranch(taskId, activityRef) {
9
36
  return `tutti/run/${taskId}/${activityRef}`;
10
37
  }
@@ -6,15 +6,14 @@ export declare const ArtifactStaticManifestSchema: import("@sinclair/typebox").T
6
6
  kind: import("@sinclair/typebox").TLiteral<"static">;
7
7
  root: import("@sinclair/typebox").TString;
8
8
  entry: import("@sinclair/typebox").TString;
9
- network: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
9
+ network: import("@sinclair/typebox").TBoolean;
10
10
  }>;
11
11
  export declare const ArtifactServerManifestSchema: import("@sinclair/typebox").TObject<{
12
12
  title: import("@sinclair/typebox").TString;
13
13
  kind: import("@sinclair/typebox").TLiteral<"server">;
14
14
  cwd: import("@sinclair/typebox").TString;
15
- script: import("@sinclair/typebox").TLiteral<"artifact:preview">;
16
- ready_path: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
17
- network: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
15
+ entry: import("@sinclair/typebox").TString;
16
+ network: import("@sinclair/typebox").TBoolean;
18
17
  }>;
19
18
  export declare const ArtifactManifestSchema: import("@sinclair/typebox").TObject<{
20
19
  version: import("@sinclair/typebox").TLiteral<1>;
@@ -23,14 +22,13 @@ export declare const ArtifactManifestSchema: import("@sinclair/typebox").TObject
23
22
  kind: import("@sinclair/typebox").TLiteral<"static">;
24
23
  root: import("@sinclair/typebox").TString;
25
24
  entry: import("@sinclair/typebox").TString;
26
- network: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
25
+ network: import("@sinclair/typebox").TBoolean;
27
26
  }>, import("@sinclair/typebox").TObject<{
28
27
  title: import("@sinclair/typebox").TString;
29
28
  kind: import("@sinclair/typebox").TLiteral<"server">;
30
29
  cwd: import("@sinclair/typebox").TString;
31
- script: import("@sinclair/typebox").TLiteral<"artifact:preview">;
32
- ready_path: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
33
- network: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
30
+ entry: import("@sinclair/typebox").TString;
31
+ network: import("@sinclair/typebox").TBoolean;
34
32
  }>]>;
35
33
  }>;
36
34
  export declare const ArtifactPreviewStatusSchema: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"ready">, import("@sinclair/typebox").TLiteral<"starting">, import("@sinclair/typebox").TLiteral<"stopped">, import("@sinclair/typebox").TLiteral<"failed">]>;
@@ -94,6 +92,28 @@ export declare const GetArtifactsResponseSchema: import("@sinclair/typebox").TOb
94
92
  }>;
95
93
  }>]>;
96
94
  }>;
95
+ export declare const IssueArtifactPreviewSessionPayloadSchema: import("@sinclair/typebox").TObject<{}>;
96
+ export declare const IssueArtifactPreviewSessionDispositionSchema: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
97
+ kind: import("@sinclair/typebox").TLiteral<"issued">;
98
+ }>, import("@sinclair/typebox").TObject<{
99
+ kind: import("@sinclair/typebox").TLiteral<"not_declared">;
100
+ }>, import("@sinclair/typebox").TObject<{
101
+ kind: import("@sinclair/typebox").TLiteral<"invalid_manifest">;
102
+ message: import("@sinclair/typebox").TString;
103
+ }>, import("@sinclair/typebox").TObject<{
104
+ kind: import("@sinclair/typebox").TLiteral<"preview_not_ready">;
105
+ status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"starting">, import("@sinclair/typebox").TLiteral<"stopped">, import("@sinclair/typebox").TLiteral<"failed">]>;
106
+ message: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
107
+ }>, import("@sinclair/typebox").TObject<{
108
+ kind: import("@sinclair/typebox").TLiteral<"interrupted">;
109
+ retry_with_new_command_id: import("@sinclair/typebox").TLiteral<true>;
110
+ }>]>;
111
+ export declare const IssueArtifactPreviewSessionResultSchema: import("@sinclair/typebox").TObject<{
112
+ bootstrap_url: import("@sinclair/typebox").TString;
113
+ expires_at: import("@sinclair/typebox").TString;
114
+ }>;
115
+ export declare const IssueArtifactPreviewSessionBodySchema: import("@sinclair/typebox").TSchema;
116
+ export declare const IssueArtifactPreviewSessionResponseSchema: import("@sinclair/typebox").TSchema;
97
117
  export declare const ArtifactPreviewCommandPayloadSchema: import("@sinclair/typebox").TObject<{}>;
98
118
  export declare const StartArtifactPreviewDispositionSchema: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TObject<{
99
119
  kind: import("@sinclair/typebox").TLiteral<"started">;
@@ -1,23 +1,23 @@
1
1
  import { Type } from "@sinclair/typebox";
2
+ import { IsoDateTimeStringSchema } from "../domain/index.js";
2
3
  import { InterruptedCommandDispositionSchema, NonEmptyStringSchema, commandEnvelopeSchema, commandResponseWithResultSchema, } from "./primitives.js";
3
- import { ViewerPathSchema } from "./viewer-reference.js";
4
4
  export const ArtifactManifestPathSchema = Type.Literal("tutti.artifact.json");
5
5
  export const ArtifactKindSchema = Type.Union([Type.Literal("static"), Type.Literal("server")]);
6
- export const ArtifactTitleSchema = Type.String({ minLength: 1, maxLength: 120 });
6
+ export const ArtifactTitleSchema = Type.String({ minLength: 1, maxLength: 120, pattern: "\\S" });
7
+ const ArtifactRepoPathSchema = Type.String({ minLength: 1 });
7
8
  export const ArtifactStaticManifestSchema = Type.Object({
8
9
  title: ArtifactTitleSchema,
9
10
  kind: Type.Literal("static"),
10
- root: ViewerPathSchema,
11
- entry: ViewerPathSchema,
12
- network: Type.Optional(Type.Boolean()),
11
+ root: ArtifactRepoPathSchema,
12
+ entry: ArtifactRepoPathSchema,
13
+ network: Type.Boolean(),
13
14
  }, { additionalProperties: false });
14
15
  export const ArtifactServerManifestSchema = Type.Object({
15
16
  title: ArtifactTitleSchema,
16
17
  kind: Type.Literal("server"),
17
- cwd: ViewerPathSchema,
18
- script: Type.Literal("artifact:preview"),
19
- ready_path: Type.Optional(Type.String({ minLength: 1, maxLength: 256, pattern: "^/" })),
20
- network: Type.Optional(Type.Boolean()),
18
+ cwd: ArtifactRepoPathSchema,
19
+ entry: Type.String({ minLength: 1, maxLength: 256, pattern: "^/" }),
20
+ network: Type.Boolean(),
21
21
  }, { additionalProperties: false });
22
22
  export const ArtifactManifestSchema = Type.Object({
23
23
  version: Type.Literal(1),
@@ -62,6 +62,31 @@ export const ArtifactProjectionSchema = Type.Union([
62
62
  export const GetArtifactsResponseSchema = Type.Object({
63
63
  artifact: ArtifactProjectionSchema,
64
64
  }, { additionalProperties: false });
65
+ export const IssueArtifactPreviewSessionPayloadSchema = Type.Object({}, { additionalProperties: false });
66
+ export const IssueArtifactPreviewSessionDispositionSchema = Type.Union([
67
+ Type.Object({ kind: Type.Literal("issued") }, { additionalProperties: false }),
68
+ Type.Object({ kind: Type.Literal("not_declared") }, { additionalProperties: false }),
69
+ Type.Object({
70
+ kind: Type.Literal("invalid_manifest"),
71
+ message: NonEmptyStringSchema,
72
+ }, { additionalProperties: false }),
73
+ Type.Object({
74
+ kind: Type.Literal("preview_not_ready"),
75
+ status: Type.Union([
76
+ Type.Literal("starting"),
77
+ Type.Literal("stopped"),
78
+ Type.Literal("failed"),
79
+ ]),
80
+ message: Type.Optional(NonEmptyStringSchema),
81
+ }, { additionalProperties: false }),
82
+ InterruptedCommandDispositionSchema,
83
+ ]);
84
+ export const IssueArtifactPreviewSessionResultSchema = Type.Object({
85
+ bootstrap_url: Type.String({ minLength: 1 }),
86
+ expires_at: IsoDateTimeStringSchema,
87
+ }, { additionalProperties: false });
88
+ export const IssueArtifactPreviewSessionBodySchema = commandEnvelopeSchema(IssueArtifactPreviewSessionPayloadSchema);
89
+ export const IssueArtifactPreviewSessionResponseSchema = commandResponseWithResultSchema(IssueArtifactPreviewSessionDispositionSchema, IssueArtifactPreviewSessionResultSchema);
65
90
  export const ArtifactPreviewCommandPayloadSchema = Type.Object({}, { additionalProperties: false });
66
91
  export const StartArtifactPreviewDispositionSchema = Type.Union([
67
92
  Type.Object({ kind: Type.Literal("started") }, { additionalProperties: false }),