@xfey/tutti 0.1.96 → 0.1.98

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 (74) hide show
  1. package/dist/collaboration-state/index.d.ts +5 -5
  2. package/dist/collaboration-state/index.js +4 -4
  3. package/dist/collaboration-state/scratchpad-source-state.d.ts +6 -1
  4. package/dist/collaboration-state/scratchpad-source-state.js +37 -6
  5. package/dist/collaboration-state/storage-types.d.ts +10 -0
  6. package/dist/collaboration-state/task-compile-context.d.ts +8 -3
  7. package/dist/collaboration-state/task-compile-context.js +91 -7
  8. package/dist/collaboration-state/types.d.ts +12 -0
  9. package/dist/collaboration-state/worklist.d.ts +2 -1
  10. package/dist/collaboration-state/worklist.js +6 -0
  11. package/dist/control-plane/clarification-commands.d.ts +12 -8
  12. package/dist/control-plane/clarification-commands.js +29 -10
  13. package/dist/control-plane/dispatch-intents.d.ts +104 -0
  14. package/dist/control-plane/dispatch-intents.js +276 -0
  15. package/dist/control-plane/follow-up-run.d.ts +1 -1
  16. package/dist/control-plane/follow-up-run.js +12 -4
  17. package/dist/control-plane/follow-up-start.d.ts +3 -1
  18. package/dist/control-plane/follow-up-start.js +7 -2
  19. package/dist/control-plane/formatters.d.ts +2 -0
  20. package/dist/control-plane/formatters.js +6 -0
  21. package/dist/control-plane/index.d.ts +9 -0
  22. package/dist/control-plane/index.js +274 -13
  23. package/dist/control-plane/intent-dispatcher.d.ts +52 -0
  24. package/dist/control-plane/intent-dispatcher.js +184 -0
  25. package/dist/control-plane/reference-summary-refresh.d.ts +4 -1
  26. package/dist/control-plane/reference-summary-refresh.js +39 -4
  27. package/dist/control-plane/run-dispatch-intents.d.ts +15 -0
  28. package/dist/control-plane/run-dispatch-intents.js +40 -0
  29. package/dist/control-plane/run-retry.d.ts +1 -1
  30. package/dist/control-plane/run-retry.js +14 -6
  31. package/dist/control-plane/run-scheduler.d.ts +1 -1
  32. package/dist/control-plane/run-scheduler.js +12 -4
  33. package/dist/control-plane/scratchpad-refresh-start.d.ts +10 -2
  34. package/dist/control-plane/scratchpad-refresh-start.js +57 -8
  35. package/dist/control-plane/startup-recovery.d.ts +2 -2
  36. package/dist/control-plane/startup-recovery.js +73 -3
  37. package/dist/control-plane/task-compile-continuation.d.ts +3 -1
  38. package/dist/control-plane/task-compile-continuation.js +14 -9
  39. package/dist/control-plane/task-compile-start.d.ts +33 -1
  40. package/dist/control-plane/task-compile-start.js +244 -26
  41. package/dist/control-plane/types.d.ts +4 -2
  42. package/dist/control-plane/worklist-terminal-feedback.js +4 -4
  43. package/dist/server-shell/http/routes/agent-context-references.js +8 -14
  44. package/dist/server-shell/http/routes/project-api/openapi-viewer-routes.d.ts +0 -2
  45. package/dist/server-shell/http/routes/project-api/reference-files.js +6 -17
  46. package/dist/workspace-ops/constants.d.ts +0 -1
  47. package/dist/workspace-ops/constants.js +0 -1
  48. package/dist/workspace-ops/index.d.ts +3 -2
  49. package/dist/workspace-ops/index.js +3 -2
  50. package/dist/workspace-ops/project-doc-templates.js +1 -1
  51. package/dist/workspace-ops/reference-files.js +3 -2
  52. package/dist/workspace-ops/reference-metadata.d.ts +2 -2
  53. package/dist/workspace-ops/reference-metadata.js +8 -22
  54. package/dist/workspace-ops/reference-summaries.js +11 -17
  55. package/dist/workspace-ops/reference-tree.d.ts +11 -0
  56. package/dist/workspace-ops/reference-tree.js +34 -0
  57. package/dist/workspace-ops/viewer-git.d.ts +1 -0
  58. package/dist/workspace-ops/viewer-git.js +11 -0
  59. package/migrations/0015_control_plane_intents.sql +115 -0
  60. package/migrations/README.md +2 -1
  61. package/node_modules/@tutti/shared/dist/ids/index.d.ts +3 -0
  62. package/node_modules/@tutti/shared/dist/ids/index.js +2 -0
  63. package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.d.ts +0 -5
  64. package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.js +0 -2
  65. package/package.json +1 -1
  66. package/prompts/procedures/context-sync.md +3 -3
  67. package/prompts/procedures/reference-file-summary.md +1 -1
  68. package/prompts/procedures/task-compile.md +2 -2
  69. package/prompts/prompt-flow-map.md +1 -1
  70. package/prompts/runs/task-execute.md +2 -2
  71. package/prompts/skills/read-references/SKILL.md +2 -2
  72. package/web/assets/{homepage-motion-scene-BFo4r4nz.js → homepage-motion-scene-_nsq44Ph.js} +1 -1
  73. package/web/assets/{index-CNIl4TF8.js → index-BtTlbKEP.js} +3 -3
  74. package/web/index.html +1 -1
@@ -1,10 +1,11 @@
1
- export { DEFAULT_VIEWER_MAX_BYTES, MAX_REFERENCE_UPLOAD_BYTES, MAX_VIEWER_MAX_BYTES, PROJECT_DOC_PATHS, REFERENCE_IMAGE_ASSET_CACHE_VERSION, REFERENCE_IMAGE_PREVIEW_MAX_EDGE_PX, REFERENCE_IMAGE_THUMBNAIL_MAX_EDGE_PX, REFERENCE_DIRECTORY_PATH, REFERENCE_FILES_DIRECTORY_PATH, REFERENCE_IMAGES_DIRECTORY_PATH, REFERENCE_INDEX_PATH, REFERENCE_TUTTI_DIRECTORY_PATH, VIEWER_ASSET_READ_LIMIT_BYTES, VIEWER_HARD_READ_LIMIT_BYTES, WORKSPACE_OPS_MAINLINE_BRANCH, } from "./constants.js";
1
+ export { DEFAULT_VIEWER_MAX_BYTES, MAX_REFERENCE_UPLOAD_BYTES, MAX_VIEWER_MAX_BYTES, PROJECT_DOC_PATHS, REFERENCE_IMAGE_ASSET_CACHE_VERSION, REFERENCE_IMAGE_PREVIEW_MAX_EDGE_PX, REFERENCE_IMAGE_THUMBNAIL_MAX_EDGE_PX, REFERENCE_DIRECTORY_PATH, REFERENCE_FILES_DIRECTORY_PATH, REFERENCE_IMAGES_DIRECTORY_PATH, REFERENCE_INDEX_PATH, VIEWER_ASSET_READ_LIMIT_BYTES, VIEWER_HARD_READ_LIMIT_BYTES, WORKSPACE_OPS_MAINLINE_BRANCH, } from "./constants.js";
2
2
  export { WorkspaceOpsError } from "./errors.js";
3
3
  export { commitOwnedPaths, requireOwnedPathsWriteReady } from "./system-commits.js";
4
4
  export { collectProjectSourceDiff, createAcceptedCheckpoint, inspectProjectWorkspace, recoverAcceptedCheckpoint, } from "./project-workspace.js";
5
5
  export { initializeProjectDocs, readProjectDocsInitializationStatus, syncProjectDocs, } from "./project-docs.js";
6
6
  export { uploadReferenceFile } from "./reference-files.js";
7
- export { referenceFileCategoryForPath, referenceFileSourceForPath, referenceMediaTypeForPath, referenceTuttiGroupForPath, } from "./reference-metadata.js";
7
+ export { isReferenceFilePath, referenceFileCategoryForPath, referenceFolderGroupForPath, referenceMediaTypeForPath, } from "./reference-metadata.js";
8
+ export { readReferenceFileEntries } from "./reference-tree.js";
8
9
  export { readReferenceSummaryEntries, readReferenceSummaryIndex, readReferenceSummaryTargets, referenceSummaryProjectionForEntry, updateReferenceSummaries, } from "./reference-summaries.js";
9
10
  export { readViewerAsset, readViewerFile, readViewerSnapshot, readViewerTree } from "./viewer.js";
10
11
  //# sourceMappingURL=index.js.map
@@ -129,7 +129,7 @@ ${asBulletList([
129
129
  ...repoHints.directories.map((directory) => `Top-level directory: \`${directory}/\``),
130
130
  ...repoHints.root_files.map((file) => `Root file: \`${file}\``),
131
131
  ], "Repository structure is pending context sync.")}
132
- - Human-uploaded reference materials, when present, live under \`docs/reference/\`.
132
+ - Shared reference materials maintained by project members and Tutti, when present, live under \`docs/reference/\`.
133
133
 
134
134
  ## Naming Conventions
135
135
 
@@ -110,7 +110,7 @@ function appendReferenceIndexEntry(options) {
110
110
  ? [
111
111
  "# Reference Files",
112
112
  "",
113
- "Human-uploaded project reference files.",
113
+ "Shared project reference files maintained by project members and Tutti.",
114
114
  "",
115
115
  "## Files",
116
116
  "",
@@ -185,7 +185,8 @@ export function uploadReferenceFile(options) {
185
185
  paths: [REFERENCE_INDEX_PATH, repoPath],
186
186
  message: `Add reference file: ${safeName}`,
187
187
  });
188
- const resolvedCommitOid = commitOid ?? readViewerFile({ workspaceRoot: options.workspaceRoot, path: repoPath }).snapshot.head.oid;
188
+ const resolvedCommitOid = commitOid ??
189
+ readViewerFile({ workspaceRoot: options.workspaceRoot, path: repoPath }).snapshot.head.oid;
189
190
  const file = readViewerFile({
190
191
  workspaceRoot: options.workspaceRoot,
191
192
  path: repoPath,
@@ -9,7 +9,7 @@ export declare function classifyReferenceUpload(options: {
9
9
  directoryPath: string;
10
10
  };
11
11
  export declare function referenceFileCategoryForPath(path: string): ReferenceFileCategory | undefined;
12
- export declare function referenceFileSourceForPath(path: string): "human_upload" | "tutti_output" | undefined;
13
- export declare function referenceTuttiGroupForPath(path: string): string | undefined;
12
+ export declare function isReferenceFilePath(path: string): boolean;
13
+ export declare function referenceFolderGroupForPath(path: string): string | undefined;
14
14
  export declare function referenceMediaTypeForPath(path: string): string | undefined;
15
15
  //# sourceMappingURL=reference-metadata.d.ts.map
@@ -1,5 +1,5 @@
1
- import { extname, posix } from "node:path";
2
- import { REFERENCE_DIRECTORY_PATH, REFERENCE_FILES_DIRECTORY_PATH, REFERENCE_IMAGES_DIRECTORY_PATH, REFERENCE_INDEX_PATH, REFERENCE_TUTTI_DIRECTORY_PATH, } from "./constants.js";
1
+ import { extname } from "node:path";
2
+ import { REFERENCE_DIRECTORY_PATH, REFERENCE_FILES_DIRECTORY_PATH, REFERENCE_IMAGES_DIRECTORY_PATH, REFERENCE_INDEX_PATH, } from "./constants.js";
3
3
  const REFERENCE_IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif"]);
4
4
  const REFERENCE_IMAGE_MEDIA_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
5
5
  function normalizeMediaType(mediaType) {
@@ -54,33 +54,19 @@ export function referenceFileCategoryForPath(path) {
54
54
  if (path.startsWith(`${REFERENCE_IMAGES_DIRECTORY_PATH}/`)) {
55
55
  return "image";
56
56
  }
57
- if (path.startsWith(`${REFERENCE_FILES_DIRECTORY_PATH}/`)) {
58
- return "file";
59
- }
60
- if (path.startsWith(`${REFERENCE_TUTTI_DIRECTORY_PATH}/`)) {
61
- return "file";
62
- }
63
- if (posix.dirname(path) === REFERENCE_DIRECTORY_PATH && path !== REFERENCE_INDEX_PATH) {
57
+ if (isReferenceFilePath(path)) {
64
58
  return "file";
65
59
  }
66
60
  return undefined;
67
61
  }
68
- export function referenceFileSourceForPath(path) {
69
- if (path.startsWith(`${REFERENCE_TUTTI_DIRECTORY_PATH}/`)) {
70
- return "tutti_output";
71
- }
72
- if (path.startsWith(`${REFERENCE_FILES_DIRECTORY_PATH}/`) ||
73
- path.startsWith(`${REFERENCE_IMAGES_DIRECTORY_PATH}/`) ||
74
- (posix.dirname(path) === REFERENCE_DIRECTORY_PATH && path !== REFERENCE_INDEX_PATH)) {
75
- return "human_upload";
76
- }
77
- return undefined;
62
+ export function isReferenceFilePath(path) {
63
+ return path !== REFERENCE_INDEX_PATH && path.startsWith(`${REFERENCE_DIRECTORY_PATH}/`);
78
64
  }
79
- export function referenceTuttiGroupForPath(path) {
80
- if (!path.startsWith(`${REFERENCE_TUTTI_DIRECTORY_PATH}/`)) {
65
+ export function referenceFolderGroupForPath(path) {
66
+ if (!isReferenceFilePath(path)) {
81
67
  return undefined;
82
68
  }
83
- const rest = path.slice(REFERENCE_TUTTI_DIRECTORY_PATH.length + 1);
69
+ const rest = path.slice(REFERENCE_DIRECTORY_PATH.length + 1);
84
70
  const [first, second] = rest.split("/");
85
71
  if (first === undefined || first.length === 0 || second === undefined) {
86
72
  return undefined;
@@ -1,10 +1,10 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { redactText } from "@tutti/shared/utils";
4
- import { REFERENCE_DIRECTORY_PATH, REFERENCE_INDEX_PATH, } from "./constants.js";
4
+ import { REFERENCE_INDEX_PATH } from "./constants.js";
5
5
  import { referenceFileCategoryForPath, referenceMediaTypeForPath } from "./reference-metadata.js";
6
+ import { readReferenceFileEntries } from "./reference-tree.js";
6
7
  import { commitOwnedPaths, requireOwnedPathsWriteReady } from "./system-commits.js";
7
- import { readViewerTree } from "./viewer.js";
8
8
  const SUMMARY_SECTION_HEADING = "## Summaries";
9
9
  const SUMMARY_BLOCK_START = "<!-- tutti-reference-summaries:v1";
10
10
  const SUMMARY_BLOCK_END = "-->";
@@ -115,7 +115,7 @@ function readReferenceIndexContent(workspaceRoot) {
115
115
  return [
116
116
  "# Reference Files",
117
117
  "",
118
- "Human-uploaded project reference files.",
118
+ "Shared project reference files maintained by project members and Tutti.",
119
119
  "",
120
120
  "## Files",
121
121
  "",
@@ -138,8 +138,7 @@ export function referenceSummaryProjectionForEntry(options) {
138
138
  ...(record.source_blob_oid === undefined ? {} : { source_blob_oid: record.source_blob_oid }),
139
139
  ...(record.read_paths === undefined ? {} : { read_paths: record.read_paths }),
140
140
  };
141
- if (options.entry.blob_oid !== undefined &&
142
- record.source_blob_oid !== options.entry.blob_oid) {
141
+ if (options.entry.blob_oid !== undefined && record.source_blob_oid !== options.entry.blob_oid) {
143
142
  return {
144
143
  status: "stale",
145
144
  ...base,
@@ -165,14 +164,8 @@ export function referenceSummaryProjectionForEntry(options) {
165
164
  ...(record.summary === undefined ? {} : { text: record.summary }),
166
165
  };
167
166
  }
168
- function readReferenceFileEntries(options) {
169
- const tree = readViewerTree({
170
- workspaceRoot: options.workspaceRoot,
171
- path: REFERENCE_DIRECTORY_PATH,
172
- depth: 2,
173
- ...(options.now === undefined ? {} : { now: options.now }),
174
- });
175
- return tree.items
167
+ function readSummaryFileEntries(options) {
168
+ return readReferenceFileEntries({ workspaceRoot: options.workspaceRoot })
176
169
  .filter((entry) => entry.kind === "file")
177
170
  .filter((entry) => referenceFileCategoryForPath(entry.path) !== undefined)
178
171
  .filter((entry) => entry.blob_oid !== undefined && entry.size_bytes !== undefined)
@@ -181,7 +174,7 @@ function readReferenceFileEntries(options) {
181
174
  export function readReferenceSummaryEntries(options) {
182
175
  const summaries = readReferenceSummaryIndex({ workspaceRoot: options.workspaceRoot });
183
176
  const requested = options.paths === undefined ? undefined : new Set(options.paths);
184
- return readReferenceFileEntries(options)
177
+ return readSummaryFileEntries(options)
185
178
  .filter((entry) => requested === undefined || requested.has(entry.path))
186
179
  .map((entry) => {
187
180
  const category = referenceFileCategoryForPath(entry.path) ?? "file";
@@ -239,7 +232,9 @@ function renderSummaryRecord(record) {
239
232
  return rendered;
240
233
  }
241
234
  function renderSummariesSection(records) {
242
- const items = records.map(renderSummaryRecord).sort((left, right) => left.path.localeCompare(right.path, "en", { sensitivity: "base" }));
235
+ const items = records
236
+ .map(renderSummaryRecord)
237
+ .sort((left, right) => left.path.localeCompare(right.path, "en", { sensitivity: "base" }));
243
238
  const json = JSON.stringify({ items }, null, 2);
244
239
  const lines = [
245
240
  SUMMARY_SECTION_HEADING,
@@ -277,9 +272,8 @@ export function updateReferenceSummaries(options) {
277
272
  }
278
273
  existing.set(summary.path, rendered);
279
274
  }
280
- const currentPaths = new Set(readReferenceFileEntries({
275
+ const currentPaths = new Set(readSummaryFileEntries({
281
276
  workspaceRoot: options.workspaceRoot,
282
- ...(options.now === undefined ? {} : { now: options.now }),
283
277
  }).map((entry) => entry.path));
284
278
  const records = [...existing.values()].filter((record) => currentPaths.has(record.path));
285
279
  const nextContent = `${removeSummarySection(existingContent)}\n\n${renderSummariesSection(records)}\n`;
@@ -0,0 +1,11 @@
1
+ import type { ViewerTreeEntry } from "./types.js";
2
+ /**
3
+ * Enumerate every committed regular file in the shared Reference directory.
4
+ * The generic Repo Viewer intentionally keeps its bounded depth contract; this
5
+ * Reference-specific reader is the single recursive source for the API,
6
+ * summaries, and agent context.
7
+ */
8
+ export declare function readReferenceFileEntries(options: {
9
+ workspaceRoot: string;
10
+ }): ViewerTreeEntry[];
11
+ //# sourceMappingURL=reference-tree.d.ts.map
@@ -0,0 +1,34 @@
1
+ import { posix } from "node:path";
2
+ import { REFERENCE_DIRECTORY_PATH } from "./constants.js";
3
+ import { WorkspaceOpsError } from "./errors.js";
4
+ import { entryKind, lsPath, lsTreeRecursive } from "./viewer-git.js";
5
+ import { isPathAllowedForProjection } from "./viewer-paths.js";
6
+ import { createViewerTreeEntry } from "./viewer-tree.js";
7
+ /**
8
+ * Enumerate every committed regular file in the shared Reference directory.
9
+ * The generic Repo Viewer intentionally keeps its bounded depth contract; this
10
+ * Reference-specific reader is the single recursive source for the API,
11
+ * summaries, and agent context.
12
+ */
13
+ export function readReferenceFileEntries(options) {
14
+ const root = lsPath(options.workspaceRoot, REFERENCE_DIRECTORY_PATH);
15
+ if (root === null) {
16
+ throw new WorkspaceOpsError("not_found", "Reference directory was not found");
17
+ }
18
+ if (entryKind(root) !== "directory") {
19
+ throw new WorkspaceOpsError("validation_failed", "Reference path is not a directory");
20
+ }
21
+ return lsTreeRecursive(options.workspaceRoot, REFERENCE_DIRECTORY_PATH)
22
+ .filter((entry) => entryKind(entry) === "file")
23
+ .filter((entry) => isPathAllowedForProjection(entry.path))
24
+ .map((entry) => {
25
+ const relativePath = entry.path.slice(REFERENCE_DIRECTORY_PATH.length + 1);
26
+ return createViewerTreeEntry(entry, {
27
+ path: entry.path,
28
+ parentPath: posix.dirname(entry.path),
29
+ depth: relativePath.split("/").length,
30
+ });
31
+ })
32
+ .sort((left, right) => left.path.localeCompare(right.path, "en", { sensitivity: "base" }));
33
+ }
34
+ //# sourceMappingURL=reference-tree.js.map
@@ -9,6 +9,7 @@ export type RawTreeEntry = {
9
9
  export declare function readViewerSnapshot(options: WorkspaceViewerOptions): ViewerRepoSnapshot;
10
10
  export declare function lsPath(workspaceRoot: string, normalizedPath: string): RawTreeEntry | null;
11
11
  export declare function lsDirectoryChildren(workspaceRoot: string, normalizedPath: string): RawTreeEntry[];
12
+ export declare function lsTreeRecursive(workspaceRoot: string, normalizedPath: string): RawTreeEntry[];
12
13
  export declare function entryKind(entry: RawTreeEntry): ViewerEntryKind;
13
14
  export declare function requireFileEntry(options: {
14
15
  workspaceRoot: string;
@@ -76,6 +76,17 @@ export function lsDirectoryChildren(workspaceRoot, normalizedPath) {
76
76
  : `${WORKSPACE_OPS_MAINLINE_BRANCH}:${normalizedPath}`;
77
77
  return lsTree(workspaceRoot, ["ls-tree", "-z", "-l", treeish]);
78
78
  }
79
+ export function lsTreeRecursive(workspaceRoot, normalizedPath) {
80
+ return lsTree(workspaceRoot, [
81
+ "ls-tree",
82
+ "-r",
83
+ "-z",
84
+ "-l",
85
+ WORKSPACE_OPS_MAINLINE_BRANCH,
86
+ "--",
87
+ normalizedPath,
88
+ ]);
89
+ }
79
90
  export function entryKind(entry) {
80
91
  if (entry.mode === "040000" || entry.type === "tree") {
81
92
  return "directory";
@@ -0,0 +1,115 @@
1
+ CREATE TABLE control_plane_intents (
2
+ intent_id TEXT PRIMARY KEY CHECK (
3
+ intent_id GLOB 'intent_[0-7][0-9A-HJKMNP-TV-Z][0-9A-HJKMNP-TV-Z][0-9A-HJKMNP-TV-Z]*'
4
+ AND length(intent_id) = 33
5
+ ),
6
+ kind TEXT NOT NULL CHECK (
7
+ kind IN (
8
+ 'task_compile_start',
9
+ 'task_compile_continuation',
10
+ 'task_bound_follow_up',
11
+ 'scratchpad_refresh',
12
+ 'reference_summary_refresh',
13
+ 'run_dispatch'
14
+ )
15
+ ),
16
+ owner_kind TEXT NOT NULL CHECK (
17
+ owner_kind IN (
18
+ 'task_compile',
19
+ 'clarification_round',
20
+ 'scratchpad',
21
+ 'reference_summary',
22
+ 'task_run'
23
+ )
24
+ ),
25
+ owner_ref TEXT NOT NULL CHECK (length(owner_ref) BETWEEN 1 AND 512),
26
+ dedupe_key TEXT NOT NULL UNIQUE CHECK (length(dedupe_key) BETWEEN 1 AND 512),
27
+ lane TEXT NOT NULL CHECK (lane IN ('foreground', 'scratchpad_background', 'run')),
28
+ priority INTEGER NOT NULL DEFAULT 100 CHECK (priority >= 0),
29
+ state TEXT NOT NULL DEFAULT 'pending' CHECK (
30
+ state IN ('pending', 'claimed', 'running', 'completed', 'failed', 'cancelled')
31
+ ),
32
+ available_at TEXT NOT NULL,
33
+ claim_token TEXT CHECK (claim_token IS NULL OR length(claim_token) BETWEEN 1 AND 256),
34
+ lease_expires_at TEXT,
35
+ attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
36
+ last_error_code TEXT CHECK (
37
+ last_error_code IS NULL OR length(last_error_code) BETWEEN 1 AND 128
38
+ ),
39
+ created_at TEXT NOT NULL,
40
+ updated_at TEXT NOT NULL,
41
+ completed_at TEXT,
42
+ CHECK (
43
+ (
44
+ state = 'pending'
45
+ AND claim_token IS NULL
46
+ AND lease_expires_at IS NULL
47
+ AND completed_at IS NULL
48
+ )
49
+ OR (
50
+ state IN ('claimed', 'running')
51
+ AND claim_token IS NOT NULL
52
+ AND lease_expires_at IS NOT NULL
53
+ AND completed_at IS NULL
54
+ )
55
+ OR (
56
+ state IN ('completed', 'failed', 'cancelled')
57
+ AND claim_token IS NULL
58
+ AND lease_expires_at IS NULL
59
+ AND completed_at IS NOT NULL
60
+ )
61
+ )
62
+ );
63
+
64
+ CREATE INDEX idx_control_plane_intents_dispatch
65
+ ON control_plane_intents (state, available_at ASC, priority ASC, created_at ASC, intent_id ASC);
66
+
67
+ CREATE INDEX idx_control_plane_intents_expired_claim
68
+ ON control_plane_intents (state, lease_expires_at ASC)
69
+ WHERE state IN ('claimed', 'running');
70
+
71
+ CREATE INDEX idx_control_plane_intents_owner
72
+ ON control_plane_intents (owner_kind, owner_ref, created_at ASC);
73
+
74
+ CREATE TABLE pending_task_compile_submissions (
75
+ id TEXT PRIMARY KEY CHECK (id = 'current'),
76
+ workflow_ref TEXT NOT NULL UNIQUE,
77
+ accepted_activity_ref TEXT NOT NULL,
78
+ requested_at TEXT NOT NULL,
79
+ source_window_after_created_at TEXT,
80
+ source_window_after_message_id TEXT,
81
+ source_window_through_created_at TEXT,
82
+ source_window_through_message_id TEXT,
83
+ CHECK (
84
+ (source_window_after_created_at IS NULL AND source_window_after_message_id IS NULL)
85
+ OR (
86
+ source_window_after_created_at IS NOT NULL
87
+ AND source_window_after_message_id IS NOT NULL
88
+ )
89
+ ),
90
+ CHECK (
91
+ (source_window_through_created_at IS NULL AND source_window_through_message_id IS NULL)
92
+ OR (
93
+ source_window_through_created_at IS NOT NULL
94
+ AND source_window_through_message_id IS NOT NULL
95
+ )
96
+ ),
97
+ CHECK (
98
+ source_window_after_created_at IS NULL
99
+ OR source_window_through_created_at IS NULL
100
+ OR source_window_through_created_at > source_window_after_created_at
101
+ OR (
102
+ source_window_through_created_at = source_window_after_created_at
103
+ AND source_window_through_message_id >= source_window_after_message_id
104
+ )
105
+ )
106
+ );
107
+
108
+ CREATE TRIGGER pending_task_compile_submissions_immutable
109
+ BEFORE UPDATE ON pending_task_compile_submissions
110
+ BEGIN
111
+ SELECT RAISE(ABORT, 'pending Task Compile submission is immutable');
112
+ END;
113
+
114
+ ALTER TABLE scratchpad_source_state
115
+ ADD COLUMN in_flight_intent_id TEXT REFERENCES control_plane_intents(intent_id);
@@ -27,10 +27,11 @@
27
27
  - `0012_scratchpad_receipts.sql`:为当前 Scratchpad 增加 `submitted` 状态字段,并新增已提交 Scratchpad 小票历史表。
28
28
  - `0013_scratchpad_receipt_sources.sql`:新增当前 Scratchpad snapshot 与 submitted receipt 的 source message id 引用表,供 Timeline Task Accepted 追溯原始聊天依据。
29
29
  - `0014_scratchpad_task_compile_context.sql`:把 Scratchpad source state 收敛为纯主群聊周期 cursor,新增 durable active Task Compile Context 与完整 source refs,并把 Current Scratchpad / receipt sources 限定为主群聊。该开发阶段 migration 不迁移旧调试数据,已有本地开发数据库需要 reset / recreate。
30
+ - `0015_control_plane_intents.sql`:新增 Control Plane durable dispatch intent,保存 bounded kind / owner / dedupe / lane、pending-to-running claim lease 与 terminal 状态,为 Clarification、Task Compile、Scratchpad、Reference Summary 和 Run 提供可恢复的 runtime handoff;同时用 immutable pending Task Compile submission row 固化 context capture 前的 workflow / main-chat cursor boundary,不保存 prompt、消息正文、provider output、secret 或 host path。
30
31
 
31
32
  ## 当前实现状态
32
33
 
33
- - `0001_init.sql` 至 `0014_scratchpad_task_compile_context.sql` 已存在,代表当前 host project SQLite schema contract。`0014` 明确采用开发数据库重建策略,不提供旧 Scratchpad source/cursor 数据兼容。
34
+ - `0001_init.sql` 至 `0015_control_plane_intents.sql` 已存在,代表当前 host project SQLite schema contract。`0014` 明确采用开发数据库重建策略,不提供旧 Scratchpad source/cursor 数据兼容。
34
35
  - SQLite adapter、migration runner、checksum 校验、store metadata 初始化、transaction helper 与 command recovery scan helper 已实现。
35
36
  - 协作 repository 与 Control Plane 已实现第一批业务读写;Phase 7 首批已复用 `0001_init.sql` 中的 `tasks.latest_run_result_id`、`task_details.result_json` 与 `run_results` 表完成 Run result / checks / promotion 摘要持久化。Control Plane recovery resolver 与 task-bound continuation / retry 复用现有 schema;新 Run 的 accepted checkpoint 业务 truth 继续写入最终 Run result / task detail result,Git tag / commit trailer 承担 crash recovery evidence。Timeline 默认从 `tasks`、`run_results` 与历史主群聊 `refs.worklist_feedback` 投影任务级审计节点,当前 task terminal Chat feedback 写入 `refs.task_result` 消息;`project_timeline_events` 只保留缺少稳定 record 的轻量可观测性事件和历史 reconcile 兼容记录。project workspace、working diff 与 ignored runtime state 都不进入 SQLite truth。`provider_usage_events` 只保存 token 计数、调用来源、模型名、记录时间和可空 anchor,不保存 provider secret、prompt 或 raw response。store 基础设施本身已经可以创建并迁移 `tutti.sqlite`。
36
37
 
@@ -25,6 +25,7 @@ export declare const ID_PREFIXES: {
25
25
  readonly hostConnection: "host_connection_";
26
26
  readonly joinToken: "join_token_";
27
27
  readonly notice: "notice_";
28
+ readonly controlPlaneIntent: "intent_";
28
29
  };
29
30
  export type IdPrefix = (typeof ID_PREFIXES)[keyof typeof ID_PREFIXES];
30
31
  export type PrefixedId<TPrefix extends string> = `${TPrefix}${string}`;
@@ -53,6 +54,7 @@ export type RelayDesktopEntryContinuationRef = PrefixedId<typeof ID_PREFIXES.rel
53
54
  export type HostConnectionRef = PrefixedId<typeof ID_PREFIXES.hostConnection>;
54
55
  export type JoinTokenRef = PrefixedId<typeof ID_PREFIXES.joinToken>;
55
56
  export type NoticeId = PrefixedId<typeof ID_PREFIXES.notice>;
57
+ export type ControlPlaneIntentId = PrefixedId<typeof ID_PREFIXES.controlPlaneIntent>;
56
58
  export declare function createPrefixedId<const TPrefix extends IdPrefix>(prefix: TPrefix): PrefixedId<TPrefix>;
57
59
  export declare function isUlidBody(value: string): boolean;
58
60
  export declare function getPrefixedIdBody(value: string, prefix: IdPrefix): string | null;
@@ -85,4 +87,5 @@ export declare const createRelayDesktopEntryContinuationRef: () => RelayDesktopE
85
87
  export declare const createHostConnectionRef: () => HostConnectionRef;
86
88
  export declare const createJoinTokenRef: () => JoinTokenRef;
87
89
  export declare const createNoticeId: () => NoticeId;
90
+ export declare const createControlPlaneIntentId: () => ControlPlaneIntentId;
88
91
  //# sourceMappingURL=index.d.ts.map
@@ -27,6 +27,7 @@ export const ID_PREFIXES = {
27
27
  hostConnection: "host_connection_",
28
28
  joinToken: "join_token_",
29
29
  notice: "notice_",
30
+ controlPlaneIntent: "intent_",
30
31
  };
31
32
  export function createPrefixedId(prefix) {
32
33
  return `${prefix}${ulid()}`;
@@ -87,4 +88,5 @@ export const createRelayDesktopEntryContinuationRef = () => createPrefixedId(ID_
87
88
  export const createHostConnectionRef = () => createPrefixedId(ID_PREFIXES.hostConnection);
88
89
  export const createJoinTokenRef = () => createPrefixedId(ID_PREFIXES.joinToken);
89
90
  export const createNoticeId = () => createPrefixedId(ID_PREFIXES.notice);
91
+ export const createControlPlaneIntentId = () => createPrefixedId(ID_PREFIXES.controlPlaneIntent);
90
92
  //# sourceMappingURL=index.js.map
@@ -250,7 +250,6 @@ export declare const ReferenceDirectoryProjectionSchema: import("@sinclair/typeb
250
250
  index_path: import("@sinclair/typebox").TLiteral<"docs/reference/README.md">;
251
251
  files_path: import("@sinclair/typebox").TLiteral<"docs/reference/files">;
252
252
  images_path: import("@sinclair/typebox").TLiteral<"docs/reference/images">;
253
- tutti_path: import("@sinclair/typebox").TLiteral<"docs/reference/tutti">;
254
253
  index_status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"available">, import("@sinclair/typebox").TLiteral<"missing">, import("@sinclair/typebox").TLiteral<"unreadable">]>;
255
254
  index_unreadable_reason: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"restricted_path">, import("@sinclair/typebox").TLiteral<"secret_like">, import("@sinclair/typebox").TLiteral<"binary">, import("@sinclair/typebox").TLiteral<"unsupported_encoding">, import("@sinclair/typebox").TLiteral<"too_large">]>>;
256
255
  }>;
@@ -266,7 +265,6 @@ export declare const ReferenceSummaryProjectionSchema: import("@sinclair/typebox
266
265
  export declare const ReferenceFileProjectionSchema: import("@sinclair/typebox").TObject<{
267
266
  path: import("@sinclair/typebox").TString;
268
267
  name: import("@sinclair/typebox").TString;
269
- source: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"human_upload">, import("@sinclair/typebox").TLiteral<"tutti_output">]>;
270
268
  category: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"file">, import("@sinclair/typebox").TLiteral<"image">]>;
271
269
  size_bytes: import("@sinclair/typebox").TInteger;
272
270
  media_type: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
@@ -308,14 +306,12 @@ export declare const GetReferenceFilesResponseSchema: import("@sinclair/typebox"
308
306
  index_path: import("@sinclair/typebox").TLiteral<"docs/reference/README.md">;
309
307
  files_path: import("@sinclair/typebox").TLiteral<"docs/reference/files">;
310
308
  images_path: import("@sinclair/typebox").TLiteral<"docs/reference/images">;
311
- tutti_path: import("@sinclair/typebox").TLiteral<"docs/reference/tutti">;
312
309
  index_status: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"available">, import("@sinclair/typebox").TLiteral<"missing">, import("@sinclair/typebox").TLiteral<"unreadable">]>;
313
310
  index_unreadable_reason: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"restricted_path">, import("@sinclair/typebox").TLiteral<"secret_like">, import("@sinclair/typebox").TLiteral<"binary">, import("@sinclair/typebox").TLiteral<"unsupported_encoding">, import("@sinclair/typebox").TLiteral<"too_large">]>>;
314
311
  }>;
315
312
  items: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
316
313
  path: import("@sinclair/typebox").TString;
317
314
  name: import("@sinclair/typebox").TString;
318
- source: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"human_upload">, import("@sinclair/typebox").TLiteral<"tutti_output">]>;
319
315
  category: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"file">, import("@sinclair/typebox").TLiteral<"image">]>;
320
316
  size_bytes: import("@sinclair/typebox").TInteger;
321
317
  media_type: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
@@ -359,7 +355,6 @@ export declare const UploadReferenceFileResultSchema: import("@sinclair/typebox"
359
355
  file: import("@sinclair/typebox").TObject<{
360
356
  path: import("@sinclair/typebox").TString;
361
357
  name: import("@sinclair/typebox").TString;
362
- source: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"human_upload">, import("@sinclair/typebox").TLiteral<"tutti_output">]>;
363
358
  category: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"file">, import("@sinclair/typebox").TLiteral<"image">]>;
364
359
  size_bytes: import("@sinclair/typebox").TInteger;
365
360
  media_type: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
@@ -164,7 +164,6 @@ export const ReferenceDirectoryProjectionSchema = Type.Object({
164
164
  index_path: Type.Literal("docs/reference/README.md"),
165
165
  files_path: Type.Literal("docs/reference/files"),
166
166
  images_path: Type.Literal("docs/reference/images"),
167
- tutti_path: Type.Literal("docs/reference/tutti"),
168
167
  index_status: Type.Union([
169
168
  Type.Literal("available"),
170
169
  Type.Literal("missing"),
@@ -196,7 +195,6 @@ export const ReferenceSummaryProjectionSchema = Type.Object({
196
195
  export const ReferenceFileProjectionSchema = Type.Object({
197
196
  path: ViewerPathSchema,
198
197
  name: NonEmptyStringSchema,
199
- source: Type.Union([Type.Literal("human_upload"), Type.Literal("tutti_output")]),
200
198
  category: ReferenceFileCategorySchema,
201
199
  size_bytes: Type.Integer({ minimum: 0 }),
202
200
  media_type: Type.Optional(NonEmptyStringSchema),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfey/tutti",
3
- "version": "0.1.96",
3
+ "version": "0.1.98",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -16,9 +16,9 @@ Use repository inspection and attached skills only when useful:
16
16
 
17
17
  - Use `read-project-docs` for existing project documents and availability.
18
18
  - Use `read-worklist` when repo state or completed formal tasks can explain the current implementation stage. Do not turn task results into a changelog.
19
- - Use `read-references` only when a human-uploaded reference file's actual content matters.
19
+ - Use `read-references` only when a file in the shared project Reference space actually matters.
20
20
 
21
- Reference upload events, file names, media types, and paths do not prove file contents. Read the relevant file before relying on it.
21
+ Reference creation events, file names, media types, paths, and summaries do not prove file contents. Read the relevant file before relying on it.
22
22
 
23
23
  # Documentation Scope
24
24
 
@@ -62,5 +62,5 @@ Return only JSON matching this schema:
62
62
  If no concrete documentation update is needed, return an empty `updates` array:
63
63
 
64
64
  ```json
65
- {"summary":"Checked current docs against repository facts; no updates needed.","updates":[]}
65
+ { "summary": "Checked current docs against repository facts; no updates needed.", "updates": [] }
66
66
  ```
@@ -6,7 +6,7 @@ model_path: codex.app_server.read_only
6
6
 
7
7
  # Role
8
8
 
9
- You summarize one human-uploaded Tutti reference file.
9
+ You summarize one file from Tutti's shared project Reference space.
10
10
 
11
11
  # Runtime
12
12
 
@@ -36,9 +36,9 @@ You compile Tutti's Scratchpad into an append-only Worklist proposal for post-ba
36
36
  Tutti can expose direct user-viewable outputs through two repo-backed surfaces when a task result is meant for project members to inspect:
37
37
 
38
38
  - a single active visual artifact, declared by `tutti.artifact.json`, for previewable pages, apps, dashboards, or similar interactive outputs;
39
- - generated files under `docs/reference/tutti/**`, for reviewable files such as reports, notes, specs, exports, or similar outputs.
39
+ - generated files in the shared `docs/reference/**` space, directly at the root or under a meaningful semantic subfolder, for reviewable files such as reports, notes, specs, exports, or similar outputs.
40
40
 
41
- Treat these as delivery surfaces, not as task subjects by themselves. Human-uploaded references still live under `docs/reference/files/` or `docs/reference/images/`, and stable project facts still belong in the relevant project docs or README-style files.
41
+ Treat these as delivery surfaces, not as task subjects by themselves. Browser uploads continue to enter the Host-managed `docs/reference/files/` or `docs/reference/images/` partitions, `docs/reference/README.md` remains Host-owned, and stable project facts still belong in the relevant project docs or README-style files.
42
42
 
43
43
  When the Scratchpad clearly asks for one of these direct deliverables, include a concise task `scope` item naming the expected surface. If a completion card should call out the result or generated file name, the scope may also mention `user_note_candidate`. Do not add artifact or generated-reference outputs solely to create feedback for ordinary implementation tasks.
44
44
 
@@ -377,7 +377,7 @@ Provider raw 输出:根对象只包含 `result`;`result` 内二选一输出
377
377
  - Reference Files 不通过 built-in skill 暴露;若任务契约或 Project Docs 指向 `docs/reference/` 中资料,Codex 可直接读取相关 repo 文件正文。
378
378
  - `goal / scope` 决定本次执行边界;`source_messages` 与 compile `clarification_rounds` 只提供该边界引用的精确材料,不自动扩展任务目标。
379
379
 
380
- 输出:根对象只包含 `result`;`result` 内四选一输出 `completed.{summary,user_note_candidate}`、`completed_no_repo_changes.{summary,user_note_candidate}`、`needs_human.{title,summary,request}` 或 `failed.summary`。`user_note_candidate` 是可选最终反馈候选,字段必填但可为 `null`;生成给用户直接查看的文件应写入 `docs/reference/tutti/`。`completed_no_repo_changes` 只用于任务契约不要求 repository file changes 的情况。`needs_human` payload 必须能直接作为 task-bound clarification request 使用:解释为什么 frozen contract 下无法安全继续,并提出一个具体、可回答、非 secret 的问题。
380
+ 输出:根对象只包含 `result`;`result` 内四选一输出 `completed.{summary,user_note_candidate}`、`completed_no_repo_changes.{summary,user_note_candidate}`、`needs_human.{title,summary,request}` 或 `failed.summary`。`user_note_candidate` 是可选最终反馈候选,字段必填但可为 `null`;生成给用户直接查看的文件应写入共享 `docs/reference/` 根目录或有意义的语义子目录,同时避开 Host-owned `README.md` 和通常由浏览器上传使用的 `files/`、`images/` 分区。`completed_no_repo_changes` 只用于任务契约不要求 repository file changes 的情况。`needs_human` payload 必须能直接作为 task-bound clarification request 使用:解释为什么 frozen contract 下无法安全继续,并提出一个具体、可回答、非 secret 的问题。
381
381
 
382
382
  下游:Run pipeline 收集真实 Git diff 作为 `changed_paths`;`needs_human` 时 Control Plane 记录 `Run.needs_human`、打开 task-bound clarification 并把 task 迁回 `pending`;有 repo candidate 的 `completed` 结果先执行 Artifact candidate gate 与 checks,再在当前 `tutti/mainline` 创建 accepted commit + immutable activity tag;空 diff 的 `completed_no_repo_changes` 结果跳过 Artifact gate、checks 与 accepted checkpoint,记录 `promotion.status = not_attempted`。
383
383
 
@@ -33,9 +33,9 @@ For a continuation, the same task is already present in this conversation. The i
33
33
 
34
34
  Use attached skills only when they help the task. `read-worklist` may clarify related formal work without expanding this task. `read-project-docs` may clarify architecture, setup, and maintenance rules. User skills may guide implementation but do not override the task, command policy, workspace policy, checks, or redaction rules.
35
35
 
36
- Human-uploaded references are under `docs/reference/`. Read a relevant source file before relying on it; do not infer its contents from a name, path, media type, upload message, or summary.
36
+ Project members and Tutti share `docs/reference/**` as the member-viewable Reference space. Read a relevant source file before relying on it; do not infer its contents from a name, path, media type, upload message, or summary.
37
37
 
38
- `docs/reference/tutti/**` is reserved for generated files that project members should inspect in the References UI. Use it for explicit reviewable deliverables, not temporary output, logs, internal notes, or canonical project documentation. Use a meaningful subfolder when it helps explain a group of files.
38
+ When this task needs to create an explicit reviewable deliverable, write it directly under `docs/reference/` or use a meaningful semantic subfolder such as `docs/reference/reports/`. Do not put temporary output, logs, internal notes, or canonical project documentation there. Do not overwrite unrelated existing References. `docs/reference/files/` and `docs/reference/images/` are normally reserved for Host-managed browser uploads, and `docs/reference/README.md` is Host-owned; do not modify them unless the task explicitly requires that exact existing file.
39
39
 
40
40
  # Documentation
41
41
 
@@ -5,7 +5,7 @@ description: List and read bounded Tutti reference files when the turn has refer
5
5
 
6
6
  # Read References
7
7
 
8
- Use this skill when a Tutti app-server turn needs user-uploaded reference material.
8
+ Use this skill when a Tutti app-server turn needs material from the shared project Reference space. Files may have been uploaded by a project member or created by Tutti in an earlier Run.
9
9
 
10
10
  When a Tutti context API runtime hint is present and includes `references:list`, list available references through:
11
11
 
@@ -19,7 +19,7 @@ When a listed text file is relevant and the hint includes `references:read`, rea
19
19
 
20
20
  Read responses may include the same `summary` object alongside text or unavailable metadata.
21
21
 
22
- Read only reference files that are relevant to the current task or question. Do not infer file contents from upload messages, names, media types, or paths alone. If a file is unavailable, restricted, binary, too large, or unsupported, use the returned disposition instead of guessing.
22
+ Read only reference files that are relevant to the current task or question. Do not infer file contents from creation events, names, media types, paths, or summaries alone. If a file is unavailable, restricted, binary, too large, or unsupported, use the returned disposition instead of guessing.
23
23
 
24
24
  This skill is read-only. Do not attempt to create, update, refresh, or write reference summaries from this skill.
25
25