@xfey/tutti 0.1.50 → 0.1.52

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 (61) hide show
  1. package/dist/approvals/projections.d.ts +0 -1
  2. package/dist/approvals/projections.js +0 -1
  3. package/dist/artifacts/candidate-validator.js +3 -3
  4. package/dist/artifacts/manifest.d.ts +2 -0
  5. package/dist/artifacts/manifest.js +9 -2
  6. package/dist/artifacts/preview-runtime.js +1 -0
  7. package/dist/collaboration-state/index.d.ts +1 -1
  8. package/dist/collaboration-state/index.js +1 -1
  9. package/dist/collaboration-state/recovery.d.ts +2 -1
  10. package/dist/collaboration-state/recovery.js +16 -0
  11. package/dist/collaboration-state/types.d.ts +5 -0
  12. package/dist/control-plane/follow-up-run.js +0 -3
  13. package/dist/control-plane/formatters.d.ts +2 -4
  14. package/dist/control-plane/formatters.js +5 -11
  15. package/dist/control-plane/index.d.ts +1 -1
  16. package/dist/control-plane/run-result-recording.d.ts +2 -9
  17. package/dist/control-plane/run-result-recording.js +6 -24
  18. package/dist/control-plane/run-scheduler.js +0 -3
  19. package/dist/control-plane/run-start.js +1 -8
  20. package/dist/control-plane/startup-recovery.d.ts +1 -1
  21. package/dist/control-plane/startup-recovery.js +90 -23
  22. package/dist/control-plane/types.d.ts +4 -17
  23. package/dist/providers/openai/app-server/workspace-write-run.d.ts +0 -2
  24. package/dist/providers/openai/app-server/workspace-write-run.js +0 -1
  25. package/dist/run-pipeline/candidate-diff.js +1 -7
  26. package/dist/run-pipeline/openai.d.ts +0 -4
  27. package/dist/run-pipeline/openai.js +42 -148
  28. package/dist/run-pipeline/task-run-invocation.d.ts +1 -2
  29. package/dist/run-pipeline/task-run-invocation.js +2 -4
  30. package/dist/server-shell/cli/host-server-runtime.js +3 -10
  31. package/dist/store/lifecycle.d.ts +1 -0
  32. package/dist/store/lifecycle.js +8 -6
  33. package/dist/workspace-ops/index.d.ts +3 -2
  34. package/dist/workspace-ops/index.js +2 -1
  35. package/dist/workspace-ops/mainline.d.ts +1 -1
  36. package/dist/workspace-ops/mainline.js +1 -5
  37. package/dist/workspace-ops/project-docs.js +26 -15
  38. package/dist/workspace-ops/project-workspace.d.ts +12 -0
  39. package/dist/workspace-ops/project-workspace.js +257 -0
  40. package/dist/workspace-ops/reference-files.js +9 -9
  41. package/dist/workspace-ops/reference-summaries.js +9 -7
  42. package/dist/workspace-ops/system-commits.d.ts +7 -0
  43. package/dist/workspace-ops/system-commits.js +43 -0
  44. package/dist/workspace-ops/types.d.ts +22 -41
  45. package/migrations/README.md +2 -2
  46. package/package.json +1 -1
  47. package/prompts/README.md +2 -2
  48. package/prompts/prompt-flow-experiment-plan.md +19 -19
  49. package/prompts/prompt-flow-map.md +20 -19
  50. package/prompts/runs/README.md +5 -3
  51. package/prompts/runs/task-continuation.md +6 -4
  52. package/prompts/runs/task-retry.md +11 -10
  53. package/prompts/runs/task-run.md +6 -4
  54. package/web/assets/{homepage-motion-scene-DYV3iw6y.js → homepage-motion-scene-ENUzAyyp.js} +1 -1
  55. package/web/assets/{index-DNnf10zz.css → index-CWWkCr48.css} +1 -1
  56. package/web/assets/{index-D_bRz29f.js → index-DK8LBgHp.js} +3 -3
  57. package/web/index.html +2 -2
  58. package/dist/run-pipeline/promotion-reconcile.d.ts +0 -21
  59. package/dist/run-pipeline/promotion-reconcile.js +0 -195
  60. package/dist/workspace-ops/run-workspaces.d.ts +0 -19
  61. package/dist/workspace-ops/run-workspaces.js +0 -302
@@ -0,0 +1,257 @@
1
+ import { createHash } from "node:crypto";
2
+ import { classifyRepositoryPathMetadata } from "@tutti/shared/utils";
3
+ import { WorkspaceOpsError } from "./errors.js";
4
+ import { runGit, runGitRaw, runGitText, stagedChangesExist, summarizeGitFailure, tryGitText, } from "./git.js";
5
+ import { requireMainlineBranch } from "./mainline.js";
6
+ const MAX_RUN_DIFF_SUMMARY_BYTES = 32 * 1024;
7
+ const ACCEPTED_TAG_PREFIX = "tutti/accepted/";
8
+ function truncateText(text, maxBytes) {
9
+ const buffer = Buffer.from(text, "utf8");
10
+ if (buffer.byteLength <= maxBytes) {
11
+ return text;
12
+ }
13
+ return `${buffer.subarray(0, maxBytes).toString("utf8")}\n[truncated]`;
14
+ }
15
+ function parsePorcelainChangedPaths(output) {
16
+ const entries = output
17
+ .toString("utf8")
18
+ .split("\0")
19
+ .filter((entry) => entry.length > 0);
20
+ const paths = new Set();
21
+ const addPath = (path) => {
22
+ const classification = classifyRepositoryPathMetadata(path);
23
+ if (classification.kind === "allowed" && classification.path_kind === "project_relative") {
24
+ paths.add(classification.normalized_path);
25
+ }
26
+ };
27
+ for (let index = 0; index < entries.length; index += 1) {
28
+ const entry = entries[index];
29
+ if (entry === undefined || entry.length < 4) {
30
+ continue;
31
+ }
32
+ const status = entry.slice(0, 2);
33
+ addPath(entry.slice(3));
34
+ if (status.includes("R") || status.includes("C")) {
35
+ const originalPath = entries[index + 1];
36
+ if (originalPath !== undefined) {
37
+ addPath(originalPath);
38
+ index += 1;
39
+ }
40
+ }
41
+ }
42
+ return [...paths].sort((left, right) => left.localeCompare(right, "en"));
43
+ }
44
+ function requireGitCommandSuccess(args, workspaceRoot) {
45
+ const result = runGitRaw(args, workspaceRoot);
46
+ if (result.status !== 0) {
47
+ throw new WorkspaceOpsError("git_failed", summarizeGitFailure(args, result));
48
+ }
49
+ return result;
50
+ }
51
+ export function fingerprintProjectSourceState(workspaceRoot) {
52
+ const status = requireGitCommandSuccess(["status", "--porcelain=v1", "-z"], workspaceRoot);
53
+ const diff = requireGitCommandSuccess(["diff", "--binary", "HEAD", "--"], workspaceRoot);
54
+ const untracked = requireGitCommandSuccess(["ls-files", "--others", "--exclude-standard", "-z"], workspaceRoot);
55
+ const hash = createHash("sha256").update(status.stdout).update(diff.stdout);
56
+ for (const path of untracked.stdout.toString("utf8").split("\0")) {
57
+ if (path.length === 0) {
58
+ continue;
59
+ }
60
+ const object = requireGitCommandSuccess(["hash-object", "--no-filters", "--", path], workspaceRoot);
61
+ hash.update(path).update("\0").update(object.stdout);
62
+ }
63
+ return hash.digest("hex");
64
+ }
65
+ export function collectProjectSourceDiff(workspaceRoot) {
66
+ const status = requireGitCommandSuccess(["status", "--porcelain=v1", "-z"], workspaceRoot);
67
+ const changedPaths = parsePorcelainChangedPaths(status.stdout);
68
+ const summaryParts = [];
69
+ const statusText = runGitText(["status", "--short"], workspaceRoot);
70
+ if (statusText.length > 0) {
71
+ summaryParts.push(statusText);
72
+ }
73
+ const unstagedStat = tryGitText(["diff", "--stat"], workspaceRoot);
74
+ if (unstagedStat !== null && unstagedStat.length > 0) {
75
+ summaryParts.push(unstagedStat);
76
+ }
77
+ const stagedStat = tryGitText(["diff", "--cached", "--stat"], workspaceRoot);
78
+ if (stagedStat !== null && stagedStat.length > 0) {
79
+ summaryParts.push(stagedStat);
80
+ }
81
+ return {
82
+ has_changes: status.stdout.length > 0,
83
+ changed_paths: changedPaths,
84
+ summary: truncateText(summaryParts.join("\n\n"), MAX_RUN_DIFF_SUMMARY_BYTES),
85
+ fingerprint: fingerprintProjectSourceState(workspaceRoot),
86
+ };
87
+ }
88
+ function acceptedTagName(workspaceRoot, activityRef) {
89
+ const tagName = `${ACCEPTED_TAG_PREFIX}${activityRef}`;
90
+ const check = runGitRaw(["check-ref-format", `refs/tags/${tagName}`], workspaceRoot);
91
+ if (check.status !== 0) {
92
+ throw new WorkspaceOpsError("validation_failed", "Accepted checkpoint tag is invalid", {
93
+ reason_code: "accepted_tag_invalid",
94
+ });
95
+ }
96
+ return tagName;
97
+ }
98
+ function nearestAcceptedCommit(workspaceRoot) {
99
+ const refs = requireGitCommandSuccess([
100
+ "for-each-ref",
101
+ "--format=%(objectname) %(refname:short)",
102
+ `refs/tags/${ACCEPTED_TAG_PREFIX}`,
103
+ ], workspaceRoot).stdout
104
+ .toString("utf8")
105
+ .trim();
106
+ if (refs.length === 0) {
107
+ return null;
108
+ }
109
+ const acceptedCommits = new Set(refs
110
+ .split("\n")
111
+ .map((line) => line.trim().split(/\s+/u)[0])
112
+ .filter((oid) => oid !== undefined && oid.length > 0));
113
+ const firstParent = runGitText(["rev-list", "--first-parent", "HEAD"], workspaceRoot);
114
+ for (const oid of firstParent.split("\n")) {
115
+ if (acceptedCommits.has(oid)) {
116
+ return oid;
117
+ }
118
+ }
119
+ return null;
120
+ }
121
+ function commitMessage(workspaceRoot, commitOid) {
122
+ return runGitText(["show", "-s", "--format=%B", commitOid], workspaceRoot);
123
+ }
124
+ function trailerValue(message, key) {
125
+ const prefix = `${key}:`;
126
+ for (const line of message.split("\n").reverse()) {
127
+ if (line.startsWith(prefix)) {
128
+ const value = line.slice(prefix.length).trim();
129
+ return value.length === 0 ? null : value;
130
+ }
131
+ }
132
+ return null;
133
+ }
134
+ function requireCheckpointIdentity(options) {
135
+ const message = commitMessage(options.workspaceRoot, options.commitOid);
136
+ if (trailerValue(message, "Task") !== options.taskId ||
137
+ trailerValue(message, "Activity") !== options.activityRef) {
138
+ throw new WorkspaceOpsError("conflict", "Accepted checkpoint Git identity does not match the Run activity", { reason_code: "accepted_tag_conflict" });
139
+ }
140
+ }
141
+ function resolveTagCommit(workspaceRoot, tagName) {
142
+ return tryGitText(["rev-parse", "--verify", `${tagName}^{commit}`], workspaceRoot);
143
+ }
144
+ function createAcceptedTag(options) {
145
+ const existing = resolveTagCommit(options.workspaceRoot, options.tagName);
146
+ if (existing !== null) {
147
+ if (existing !== options.commitOid) {
148
+ throw new WorkspaceOpsError("conflict", "Accepted checkpoint tag already points to another commit", { reason_code: "accepted_tag_conflict" });
149
+ }
150
+ return;
151
+ }
152
+ runGit(["tag", options.tagName, options.commitOid], options.workspaceRoot);
153
+ }
154
+ function changedPathsForCommit(workspaceRoot, commitOid) {
155
+ const result = requireGitCommandSuccess(["diff-tree", "--root", "--no-commit-id", "--name-only", "-r", "-z", commitOid], workspaceRoot);
156
+ const paths = new Set();
157
+ for (const path of result.stdout.toString("utf8").split("\0")) {
158
+ if (path.length === 0) {
159
+ continue;
160
+ }
161
+ const classification = classifyRepositoryPathMetadata(path);
162
+ if (classification.kind === "allowed" && classification.path_kind === "project_relative") {
163
+ paths.add(classification.normalized_path);
164
+ }
165
+ }
166
+ return [...paths].sort((left, right) => left.localeCompare(right, "en"));
167
+ }
168
+ export function inspectProjectWorkspace(workspaceRoot) {
169
+ requireMainlineBranch(workspaceRoot);
170
+ return {
171
+ workspace_root: workspaceRoot,
172
+ head_ref: runGitText(["rev-parse", "--verify", "HEAD"], workspaceRoot),
173
+ previous_accepted_commit_oid: nearestAcceptedCommit(workspaceRoot),
174
+ };
175
+ }
176
+ export function recoverAcceptedCheckpoint(options) {
177
+ const inspection = inspectProjectWorkspace(options.workspaceRoot);
178
+ const tagName = acceptedTagName(options.workspaceRoot, options.activityRef);
179
+ const taggedCommit = resolveTagCommit(options.workspaceRoot, tagName);
180
+ if (taggedCommit !== null) {
181
+ requireCheckpointIdentity({
182
+ workspaceRoot: options.workspaceRoot,
183
+ commitOid: taggedCommit,
184
+ taskId: options.taskId,
185
+ activityRef: options.activityRef,
186
+ });
187
+ return {
188
+ kind: "recovered",
189
+ recovered_from: "accepted_tag",
190
+ commit_oid: taggedCommit,
191
+ accepted_tag: tagName,
192
+ changed_paths: changedPathsForCommit(options.workspaceRoot, taggedCommit),
193
+ };
194
+ }
195
+ const headMessage = commitMessage(options.workspaceRoot, inspection.head_ref);
196
+ if (trailerValue(headMessage, "Activity") !== options.activityRef) {
197
+ return { kind: "not_found" };
198
+ }
199
+ requireCheckpointIdentity({
200
+ workspaceRoot: options.workspaceRoot,
201
+ commitOid: inspection.head_ref,
202
+ taskId: options.taskId,
203
+ activityRef: options.activityRef,
204
+ });
205
+ createAcceptedTag({
206
+ workspaceRoot: options.workspaceRoot,
207
+ tagName,
208
+ commitOid: inspection.head_ref,
209
+ });
210
+ return {
211
+ kind: "recovered",
212
+ recovered_from: "head_trailer",
213
+ commit_oid: inspection.head_ref,
214
+ accepted_tag: tagName,
215
+ changed_paths: changedPathsForCommit(options.workspaceRoot, inspection.head_ref),
216
+ };
217
+ }
218
+ export function createAcceptedCheckpoint(options) {
219
+ const recovered = recoverAcceptedCheckpoint(options);
220
+ if (recovered.kind === "recovered") {
221
+ return {
222
+ promoted_commit_oid: recovered.commit_oid,
223
+ accepted_tag: recovered.accepted_tag,
224
+ disposition: "already_accepted",
225
+ };
226
+ }
227
+ const inspection = inspectProjectWorkspace(options.workspaceRoot);
228
+ if (options.expectedSourceFingerprint !== undefined &&
229
+ fingerprintProjectSourceState(options.workspaceRoot) !== options.expectedSourceFingerprint) {
230
+ throw new WorkspaceOpsError("conflict", "Project source changed after validation and before accepted checkpoint", { reason_code: "accepted_checkpoint_candidate_changed" });
231
+ }
232
+ runGit(["add", "-A"], options.workspaceRoot);
233
+ if (!stagedChangesExist(options.workspaceRoot)) {
234
+ throw new WorkspaceOpsError("validation_failed", "Accepted checkpoint has no staged source changes", { reason_code: "accepted_checkpoint_candidate_missing" });
235
+ }
236
+ const previousAccepted = inspection.previous_accepted_commit_oid ?? "none";
237
+ runGit([
238
+ "commit",
239
+ "-m",
240
+ options.summary.trim().length === 0 ? "Complete Tutti task" : options.summary.trim(),
241
+ "-m",
242
+ [
243
+ `Task: ${options.taskId}`,
244
+ `Activity: ${options.activityRef}`,
245
+ `Previous-Accepted: ${previousAccepted}`,
246
+ ].join("\n"),
247
+ ], options.workspaceRoot);
248
+ const commitOid = runGitText(["rev-parse", "--verify", "HEAD"], options.workspaceRoot);
249
+ const tagName = acceptedTagName(options.workspaceRoot, options.activityRef);
250
+ createAcceptedTag({ workspaceRoot: options.workspaceRoot, tagName, commitOid });
251
+ return {
252
+ promoted_commit_oid: commitOid,
253
+ accepted_tag: tagName,
254
+ disposition: "created",
255
+ };
256
+ }
257
+ //# sourceMappingURL=project-workspace.js.map
@@ -3,9 +3,8 @@ import { extname, join, posix } from "node:path";
3
3
  import { classifyViewerPath } from "@tutti/shared/utils";
4
4
  import { MAX_REFERENCE_UPLOAD_BYTES, REFERENCE_DIRECTORY_PATH, REFERENCE_INDEX_PATH, } from "./constants.js";
5
5
  import { WorkspaceOpsError } from "./errors.js";
6
- import { runGit, runGitText, stagedChangesExist } from "./git.js";
7
- import { requireMainlineWriteReady } from "./mainline.js";
8
6
  import { classifyReferenceUpload } from "./reference-metadata.js";
7
+ import { commitOwnedPaths, requireOwnedPathsWriteReady } from "./system-commits.js";
9
8
  import { readViewerFile } from "./viewer.js";
10
9
  function commandSuffix(commandId) {
11
10
  return commandId.slice(-8).toLowerCase();
@@ -90,7 +89,6 @@ export function uploadReferenceFile(options) {
90
89
  if (options.content.byteLength > MAX_REFERENCE_UPLOAD_BYTES) {
91
90
  throw new WorkspaceOpsError("validation_failed", `Reference file content cannot exceed ${MAX_REFERENCE_UPLOAD_BYTES} bytes`);
92
91
  }
93
- requireMainlineWriteReady(options.workspaceRoot);
94
92
  const safeName = sanitizeReferenceFileName(options.fileName, options.commandId);
95
93
  const classification = classifyReferenceUpload({
96
94
  fileName: options.fileName,
@@ -100,6 +98,7 @@ export function uploadReferenceFile(options) {
100
98
  const referenceDirectory = join(options.workspaceRoot, ...classification.directoryPath.split("/"));
101
99
  const repoPath = `${classification.directoryPath}/${safeName}`;
102
100
  const filesystemPath = join(referenceDirectory, safeName);
101
+ requireOwnedPathsWriteReady(options.workspaceRoot, [REFERENCE_INDEX_PATH, repoPath]);
103
102
  mkdirSync(referenceDirectory, { recursive: true });
104
103
  if (existsSync(filesystemPath)) {
105
104
  const existing = readFileSync(filesystemPath);
@@ -117,11 +116,12 @@ export function uploadReferenceFile(options) {
117
116
  category: classification.category,
118
117
  ...(options.note === undefined ? {} : { note: options.note }),
119
118
  });
120
- runGit(["add", "--", REFERENCE_INDEX_PATH, repoPath], options.workspaceRoot);
121
- if (stagedChangesExist(options.workspaceRoot)) {
122
- runGit(["commit", "-m", `Add reference file: ${safeName}`], options.workspaceRoot);
123
- }
124
- const commitOid = runGitText(["rev-parse", "--verify", "HEAD"], options.workspaceRoot);
119
+ const commitOid = commitOwnedPaths({
120
+ workspaceRoot: options.workspaceRoot,
121
+ paths: [REFERENCE_INDEX_PATH, repoPath],
122
+ message: `Add reference file: ${safeName}`,
123
+ });
124
+ const resolvedCommitOid = commitOid ?? readViewerFile({ workspaceRoot: options.workspaceRoot, path: repoPath }).snapshot.head.oid;
125
125
  const file = readViewerFile({
126
126
  workspaceRoot: options.workspaceRoot,
127
127
  path: repoPath,
@@ -131,7 +131,7 @@ export function uploadReferenceFile(options) {
131
131
  file: file.file,
132
132
  category: classification.category,
133
133
  index_path: REFERENCE_INDEX_PATH,
134
- commit_oid: commitOid,
134
+ commit_oid: resolvedCommitOid,
135
135
  };
136
136
  }
137
137
  //# sourceMappingURL=reference-files.js.map
@@ -2,9 +2,8 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { redactText } from "@tutti/shared/utils";
4
4
  import { REFERENCE_DIRECTORY_PATH, REFERENCE_INDEX_PATH, } from "./constants.js";
5
- import { runGit, runGitText, stagedChangesExist } from "./git.js";
6
- import { requireMainlineWriteReady } from "./mainline.js";
7
5
  import { referenceFileCategoryForPath, referenceMediaTypeForPath } from "./reference-metadata.js";
6
+ import { commitOwnedPaths, requireOwnedPathsWriteReady } from "./system-commits.js";
8
7
  import { readViewerTree } from "./viewer.js";
9
8
  const SUMMARY_SECTION_HEADING = "## Summaries";
10
9
  const SUMMARY_BLOCK_START = "<!-- tutti-reference-summaries:v1";
@@ -266,7 +265,7 @@ export function updateReferenceSummaries(options) {
266
265
  if (options.summaries.length === 0) {
267
266
  return { updated_paths: [] };
268
267
  }
269
- requireMainlineWriteReady(options.workspaceRoot);
268
+ requireOwnedPathsWriteReady(options.workspaceRoot, [REFERENCE_INDEX_PATH]);
270
269
  const existingContent = readReferenceIndexContent(options.workspaceRoot);
271
270
  const existing = parseSummaryIndex(existingContent);
272
271
  const changedPaths = [];
@@ -285,14 +284,17 @@ export function updateReferenceSummaries(options) {
285
284
  const nextContent = `${removeSummarySection(existingContent)}\n\n${renderSummariesSection(records)}\n`;
286
285
  const indexPath = join(options.workspaceRoot, "docs", "reference", "README.md");
287
286
  writeFileSync(indexPath, nextContent, "utf8");
288
- runGit(["add", "--", REFERENCE_INDEX_PATH], options.workspaceRoot);
289
- if (!stagedChangesExist(options.workspaceRoot)) {
287
+ const commitOid = commitOwnedPaths({
288
+ workspaceRoot: options.workspaceRoot,
289
+ paths: [REFERENCE_INDEX_PATH],
290
+ message: "Update reference summaries",
291
+ });
292
+ if (commitOid === null) {
290
293
  return { updated_paths: [] };
291
294
  }
292
- runGit(["commit", "-m", "Update reference summaries"], options.workspaceRoot);
293
295
  return {
294
296
  updated_paths: changedPaths.length === 0 ? options.summaries.map((summary) => summary.path) : changedPaths,
295
- commit_oid: runGitText(["rev-parse", "--verify", "HEAD"], options.workspaceRoot),
297
+ commit_oid: commitOid,
296
298
  };
297
299
  }
298
300
  //# sourceMappingURL=reference-summaries.js.map
@@ -0,0 +1,7 @@
1
+ export declare function requireOwnedPathsWriteReady(workspaceRoot: string, paths: readonly string[]): void;
2
+ export declare function commitOwnedPaths(options: {
3
+ workspaceRoot: string;
4
+ paths: readonly string[];
5
+ message: string;
6
+ }): string | null;
7
+ //# sourceMappingURL=system-commits.d.ts.map
@@ -0,0 +1,43 @@
1
+ import { classifyRepositoryPathMetadata } from "@tutti/shared/utils";
2
+ import { WorkspaceOpsError } from "./errors.js";
3
+ import { runGit, runGitRaw, runGitText, summarizeGitFailure } from "./git.js";
4
+ import { requireMainlineBranch } from "./mainline.js";
5
+ function requireOwnedPaths(paths) {
6
+ if (paths.length === 0) {
7
+ throw new WorkspaceOpsError("validation_failed", "Owned path commit requires at least one path");
8
+ }
9
+ const normalized = paths.map((path) => {
10
+ const classification = classifyRepositoryPathMetadata(path);
11
+ if (classification.kind !== "allowed" || classification.path_kind !== "project_relative") {
12
+ throw new WorkspaceOpsError("forbidden", "Owned path commit contains an invalid repo path");
13
+ }
14
+ return classification.normalized_path;
15
+ });
16
+ return [...new Set(normalized)];
17
+ }
18
+ export function requireOwnedPathsWriteReady(workspaceRoot, paths) {
19
+ requireMainlineBranch(workspaceRoot);
20
+ const ownedPaths = requireOwnedPaths(paths);
21
+ const status = runGitRaw(["status", "--porcelain=v1", "-z", "--", ...ownedPaths], workspaceRoot);
22
+ if (status.status !== 0) {
23
+ throw new WorkspaceOpsError("git_failed", summarizeGitFailure(["status", "--", ...ownedPaths], status));
24
+ }
25
+ if (status.stdout.length > 0) {
26
+ throw new WorkspaceOpsError("conflict", "System write conflicts with existing unaccepted changes on an owned path", { reason_code: "owned_path_commit_conflict" });
27
+ }
28
+ }
29
+ export function commitOwnedPaths(options) {
30
+ requireMainlineBranch(options.workspaceRoot);
31
+ const ownedPaths = requireOwnedPaths(options.paths);
32
+ runGit(["add", "-A", "--", ...ownedPaths], options.workspaceRoot);
33
+ const staged = runGitRaw(["diff", "--cached", "--quiet", "--", ...ownedPaths], options.workspaceRoot);
34
+ if (staged.status === 0) {
35
+ return null;
36
+ }
37
+ if (staged.status !== 1) {
38
+ throw new WorkspaceOpsError("git_failed", summarizeGitFailure(["diff", "--cached", "--", ...ownedPaths], staged));
39
+ }
40
+ runGit(["commit", "--only", "-m", options.message, "--", ...ownedPaths], options.workspaceRoot);
41
+ return runGitText(["rev-parse", "--verify", "HEAD"], options.workspaceRoot);
42
+ }
43
+ //# sourceMappingURL=system-commits.js.map
@@ -166,54 +166,35 @@ export type SyncProjectDocsResult = {
166
166
  updated_paths: string[];
167
167
  commit_oid?: string;
168
168
  };
169
- export type RunWorkspaceHandle = {
170
- activity_ref: ActivityRef;
171
- task_id: TaskId;
172
- workspace_root: string;
173
- run_workspace_root: string;
174
- repo_root: string;
175
- base_ref: string;
176
- temp_branch: string;
177
- };
178
- export type MergeWorkspaceHandle = {
179
- activity_ref: ActivityRef;
180
- task_id: TaskId;
169
+ export type RunDiffSummary = {
170
+ has_changes: boolean;
171
+ changed_paths: string[];
172
+ summary: string;
173
+ fingerprint: string;
174
+ };
175
+ export type ProjectWorkspaceInspection = {
181
176
  workspace_root: string;
182
- merge_workspace_root: string;
183
- repo_root: string;
184
- base_ref: string;
185
- source_base_ref: string;
186
- source_result_commit_oid: string;
187
- temp_branch: string;
188
- };
189
- export type PrepareRunWorkspaceOptions = WorkspaceViewerOptions & {
190
- runWorkspaceRoot: string;
191
- taskId: TaskId;
192
- activityRef: ActivityRef;
177
+ head_ref: string;
178
+ previous_accepted_commit_oid: string | null;
193
179
  };
194
- export type PrepareMergeWorkspaceOptions = WorkspaceViewerOptions & {
195
- mergeWorkspaceRoot: string;
180
+ export type CreateAcceptedCheckpointOptions = WorkspaceViewerOptions & {
196
181
  taskId: TaskId;
197
182
  activityRef: ActivityRef;
198
- sourceBaseRef: string;
199
- sourceCommit: ResultCommit;
200
- };
201
- export type RunDiffSummary = {
202
- changed_paths: string[];
203
183
  summary: string;
184
+ expectedSourceFingerprint?: string;
204
185
  };
205
- export type ResultCommit = {
206
- commit_oid: string;
207
- };
208
- export type PromotionResult = {
186
+ export type AcceptedCheckpointResult = {
209
187
  promoted_commit_oid: string;
188
+ accepted_tag: string;
189
+ disposition: "created" | "already_accepted";
210
190
  };
211
- export type CleanupRunWorkspaceResult = {
212
- warnings: string[];
213
- };
214
- export type CleanupStaleRunWorkspacesResult = {
215
- removed_run_workspace_count: number;
216
- removed_temp_branch_count: number;
217
- warnings: string[];
191
+ export type AcceptedCheckpointRecoveryResult = {
192
+ kind: "not_found";
193
+ } | {
194
+ kind: "recovered";
195
+ recovered_from: "accepted_tag" | "head_trailer";
196
+ commit_oid: string;
197
+ accepted_tag: string;
198
+ changed_paths: string[];
218
199
  };
219
200
  //# sourceMappingURL=types.d.ts.map
@@ -21,7 +21,7 @@
21
21
  - `0006_scratchpad_source_state.sql`:新增自动 Scratchpad refresh 的本机 source cursor / dirty / in-flight / backoff 状态表。
22
22
  - `0007_workspace_signal_read_state_scopes.sql`:扩展 read-state scope,允许 References 与 Skills 写入模块更新通知事件。
23
23
  - `0008_provider_usage_anchors.sql`:为 provider usage ledger 增加可空 activity / workflow / task / run result anchor 字段,供 Timeline 尽力把 token usage 挂到可证明的历史节点。
24
- - `0009_project_timeline_events.sql`:新增 Timeline 专用 append-only durable event 表,用于记录缺少其他稳定 record 的 context sync、reference summary promotion reconcile 轻量可观测性节点。
24
+ - `0009_project_timeline_events.sql`:新增 Timeline 专用 append-only durable event 表,用于记录缺少其他稳定 record 的 context sync、reference summary 与历史 promotion reconcile 轻量可观测性节点;新 Run 不再写 reconcile event。
25
25
  - `0010_project_brief.sql`:新增 host-local Project Brief projection 表,用于保存四个 baseline Project Docs 的短摘要和 source fingerprint。
26
26
  - `0011_message_author_avatar.sql`:为消息作者增加可选 Relay account avatar 快照,保证跨浏览器查看历史消息时不依赖当前 viewer profile 或 presence sample。
27
27
  - `0012_scratchpad_receipts.sql`:为当前 Scratchpad 增加 `submitted` 状态字段,并新增已提交 Scratchpad 小票历史表。
@@ -31,7 +31,7 @@
31
31
 
32
32
  - `0001_init.sql`、`0002_allow_sealed_round_successor_note.sql`、`0003_drop_scratchpad_source_message_refs.sql`、`0004_read_state.sql`、`0005_provider_usage.sql`、`0006_scratchpad_source_state.sql`、`0007_workspace_signal_read_state_scopes.sql`、`0008_provider_usage_anchors.sql`、`0009_project_timeline_events.sql`、`0010_project_brief.sql`、`0011_message_author_avatar.sql`、`0012_scratchpad_receipts.sql` 与 `0013_scratchpad_receipt_sources.sql` 已存在,代表当前 host project SQLite schema contract。
33
33
  - SQLite adapter、migration runner、checksum 校验、store metadata 初始化、transaction helper 与 command recovery scan helper 已实现。
34
- - 协作 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;deterministic promotion reconcile 的业务 truth 仍写入最终 Run result / task detail result,Timeline 默认从 `tasks`、`run_results` 与历史主群聊 `refs.worklist_feedback` 投影任务级审计节点,当前 task terminal Chat feedback 写入 `refs.task_result` 消息;`project_timeline_events` 只保留缺少稳定 record 的轻量可观测性事件。merge workspace 不进入 SQLite truth。`provider_usage_events` 只保存 token 计数、调用来源、模型名、记录时间和可空 anchor,不保存 provider secret、prompt 或 raw response。store 基础设施本身已经可以创建并迁移 `tutti.sqlite`。
34
+ - 协作 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`。
35
35
 
36
36
  ## PRAGMA 边界
37
37
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfey/tutti",
3
- "version": "0.1.50",
3
+ "version": "0.1.52",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/prompts/README.md CHANGED
@@ -15,7 +15,7 @@ Prompt / skill 改造后的 live 实验计划与记录见 [prompt-flow-experimen
15
15
 
16
16
  - `procedures/`:`scratchpad_refresh`、`project_brief_refresh`、`task_compile`、`project_context_bootstrap`、`context_sync`、`reference_summary_refresh`、`follow_up_check` 等 Procedure / workflow prompt。
17
17
  - `artifacts/`:候选实现完成后的 transient Artifact applicability 判断;只接收 bounded task / diff / repo evidence,不写回 Task contract 或 repo。
18
- - `runs/`:正式 task `Run`、task-bound continuation 与 pipeline self-correction retry 等 Run pipeline prompt;provider transient retry 不新增 prompt,而是复用失败 attempt 的同一 prompt family;Codex 参与式 promotion reconcile prompt 后续按 fallback 需求加入。
18
+ - `runs/`:正式 task `Run`、task-bound continuation 与 pipeline self-correction retry 等 Run pipeline prompt;provider transient retry 不新增 prompt,而是复用失败 attempt 的同一 prompt family。所有 Run prompt都以持久 project workspace为 cwd contract,不设计 promotion reconcile prompt
19
19
  - `codex/`:Codex app-server smoke 等 adapter / spike prompt。
20
20
  - `skills/`:Tutti packaged built-in Codex skills;由 server-side app-server skill selection 选择,不在 Web Skills 页面展示,也不保存 user skills。
21
21
  - `chat-assistant.md`:主群聊与 clarification round 内 `@tutti` 只读项目问答 prompt;它不是 Procedure / Run prompt。
@@ -32,6 +32,6 @@ Prompt / skill 改造后的 live 实验计划与记录见 [prompt-flow-experimen
32
32
  - 文件开头的 HTML metadata 注释块只用于维护索引;renderer 会在变量替换前剥离,不进入模型上下文。
33
33
  - renderer 会先 redaction 变量内容再做最终 secret-like 检查;repo evidence 中的 `{{ ... }}` 模板语法属于普通文件内容,不作为 Tutti prompt 占位符处理。
34
34
  - 输出结构化 schema 不写在 prompt 文件里;schema 仍由 TypeBox / JSON Schema 管理。
35
- - prompt 文件不得保存 provider secret、credential ref、host 绝对路径、run workspace path、join token、cookie 或 provider raw event。
35
+ - prompt 文件不得保存 provider secret、credential ref、host 绝对路径、project workspace path、join token、cookie 或 provider raw event。
36
36
  - built-in skill 文件不得保存 context API token、host-local URL、provider secret、host 绝对路径或 user skill 内容。
37
37
  - workflow 语义和 prompt 选择归 `control-plane/workflows`;provider adapter 只执行渲染后的 provider request。
@@ -9,9 +9,9 @@ It is an experiment scratchpad, not a product contract. Canonical runtime behavi
9
9
  ## Goals
10
10
 
11
11
  - Exercise the full user-visible loop:
12
- `main chat -> Scratchpad -> Worklist -> Run -> checks / result commit / mechanical promotion when repo candidate exists`.
12
+ `main chat -> Scratchpad -> Worklist -> Run -> Artifact gate / checks / accepted checkpoint when repo candidate exists`.
13
13
  - Verify that implementation Runs update related project documents and directory `README.md` files
14
- in the same candidate workspace when stable facts change.
14
+ in the persistent project workspace when stable facts change.
15
15
  - Verify that code-only Runs record `docs = sync_deferred` without blocking promotion.
16
16
  - Verify that independent `context_sync` remains the clean path for broader documentation
17
17
  maintenance after one or more Runs.
@@ -23,15 +23,14 @@ It is an experiment scratchpad, not a product contract. Canonical runtime behavi
23
23
 
24
24
  Current expected Run behavior:
25
25
 
26
- - `workspace_write_run` and `follow_up_run` usually produce candidate changes in a run workspace; tasks that do not require repository file changes can explicitly complete without a candidate diff.
26
+ - `workspace_write_run` and `follow_up_run` produce candidate changes in the persistent project workspace; tasks that do not require repository file changes can explicitly complete without a candidate diff.
27
27
  - Codex is instructed to update related docs as part of implementation when stable facts change.
28
28
  - Run pipeline derives docs status from Git changed paths:
29
29
  - Markdown / README / MDX changes become `docs = updated`.
30
30
  - Code-only candidates become `docs = sync_deferred`.
31
31
  - Tutti checks run after candidate diff collection when a repo candidate exists.
32
- - Passing checks allow a result commit and mechanical fast-forward promotion.
33
- - If mainline moved, deterministic promotion reconcile applies the result to latest mainline,
34
- reruns checks, creates a reconciled candidate commit, and mechanically promotes it.
32
+ - Passing checks allow an accepted commit and immutable `tutti/accepted/<activity_ref>` tag.
33
+ - Initial, continuation, provider retry, and self-correction attempts reuse the same cwd and retained source/dependency state; the pipeline does not create temporary branches or worktrees.
35
34
  - `context_sync` is the independent documentation maintenance Procedure; it is not a promotion gate.
36
35
 
37
36
  Current open issues to keep watching:
@@ -77,7 +76,7 @@ repositories are background material only; they can be trimmed or adjusted to ma
77
76
  - Purpose:
78
77
  - Fast checks.
79
78
  - Easy task-bound clarification fixture.
80
- - Deterministic promotion reconcile fixture.
79
+ - Accepted checkpoint / tag recovery fixture.
81
80
  - Adjustments:
82
81
  - Use a tiny TypeScript or JavaScript CLI.
83
82
  - Include one ambiguous data-format task that should require clarification.
@@ -89,13 +88,13 @@ repositories are background material only; they can be trimmed or adjusted to ma
89
88
 
90
89
  Purpose:
91
90
 
92
- - Verify project context bootstrap, task compile, Run, checks, result commit, and promotion.
91
+ - Verify project context bootstrap, task compile, Run, Artifact gate, checks, and accepted checkpoint.
93
92
 
94
93
  Expected:
95
94
 
96
95
  - Missing baseline context files are created before Worklist proposal.
97
96
  - First task reaches `done`.
98
- - Run result includes changed paths and promoted commit.
97
+ - Run result includes changed paths and promoted accepted commit; the immutable activity tag points to that commit.
99
98
 
100
99
  ### E2: Run Updates Related Docs
101
100
 
@@ -111,7 +110,7 @@ Expected:
111
110
 
112
111
  - Candidate changed paths include source changes and relevant Markdown / README changes.
113
112
  - Run result records `docs = updated`.
114
- - Promotion is mechanical after checks.
113
+ - Accepted checkpoint is created after Artifact validation and checks.
115
114
 
116
115
  ### E3: Code-Only Run Defers Docs
117
116
 
@@ -151,26 +150,27 @@ Purpose:
151
150
  Expected:
152
151
 
153
152
  - Failed checks produce a finished result with failed outcome.
154
- - No result commit is promoted.
153
+ - No accepted commit or activity tag is created.
155
154
  - Task remains explainable through Worklist and run result history.
156
155
 
157
- ### E6: Promotion Reconcile
156
+ ### E6: Persistent Workspace Dependency Continuity
158
157
 
159
158
  Purpose:
160
159
 
161
- - Verify deterministic reconcile after mainline moves.
160
+ - Verify candidate validation, accepted checkpoint, and formal Artifact preview use one persistent project directory without copying runtime state.
162
161
 
163
162
  Setup:
164
163
 
165
- - Start a Run.
166
- - Move mainline with a non-conflicting commit before promotion.
164
+ - Use an existing npm repo with `node_modules/` ignored and dependencies initially absent.
165
+ - Let the Run install the project-local dependency required by `npm run artifact:preview` and produce a valid Artifact candidate.
167
166
 
168
167
  Expected:
169
168
 
170
- - Initial promotion detects mainline movement.
171
- - Reconcile applies candidate changes to latest mainline.
172
- - Checks rerun on the reconciled workspace.
173
- - Mechanical promotion succeeds when checks pass.
169
+ - Provider, candidate preview, checks, accepted checkpoint, and formal preview resolve the same project workspace root.
170
+ - Candidate validation and checks pass, then accepted commit + immutable activity tag are created.
171
+ - `node_modules/` is not committed but remains available in the project directory.
172
+ - Formal preview and a later preview restart succeed without reinstalling or restoring a prepared snapshot.
173
+ - No `tutti/run/*`, `tutti/merge/*`, extra worktree, or promotion reconcile event is created.
174
174
 
175
175
  ### E7: Task-Bound Clarification
176
176