@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.
- package/dist/approvals/projections.d.ts +0 -1
- package/dist/approvals/projections.js +0 -1
- package/dist/artifacts/candidate-validator.js +3 -3
- package/dist/artifacts/manifest.d.ts +2 -0
- package/dist/artifacts/manifest.js +9 -2
- package/dist/artifacts/preview-runtime.js +1 -0
- package/dist/collaboration-state/index.d.ts +1 -1
- package/dist/collaboration-state/index.js +1 -1
- package/dist/collaboration-state/recovery.d.ts +2 -1
- package/dist/collaboration-state/recovery.js +16 -0
- package/dist/collaboration-state/types.d.ts +5 -0
- package/dist/control-plane/follow-up-run.js +0 -3
- package/dist/control-plane/formatters.d.ts +2 -4
- package/dist/control-plane/formatters.js +5 -11
- package/dist/control-plane/index.d.ts +1 -1
- package/dist/control-plane/run-result-recording.d.ts +2 -9
- package/dist/control-plane/run-result-recording.js +6 -24
- package/dist/control-plane/run-scheduler.js +0 -3
- package/dist/control-plane/run-start.js +1 -8
- package/dist/control-plane/startup-recovery.d.ts +1 -1
- package/dist/control-plane/startup-recovery.js +90 -23
- package/dist/control-plane/types.d.ts +4 -17
- package/dist/providers/openai/app-server/workspace-write-run.d.ts +0 -2
- package/dist/providers/openai/app-server/workspace-write-run.js +0 -1
- package/dist/run-pipeline/candidate-diff.js +1 -7
- package/dist/run-pipeline/openai.d.ts +0 -4
- package/dist/run-pipeline/openai.js +42 -148
- package/dist/run-pipeline/task-run-invocation.d.ts +1 -2
- package/dist/run-pipeline/task-run-invocation.js +2 -4
- package/dist/server-shell/cli/host-server-runtime.js +3 -10
- package/dist/store/lifecycle.d.ts +1 -0
- package/dist/store/lifecycle.js +8 -6
- package/dist/workspace-ops/index.d.ts +3 -2
- package/dist/workspace-ops/index.js +2 -1
- package/dist/workspace-ops/mainline.d.ts +1 -1
- package/dist/workspace-ops/mainline.js +1 -5
- package/dist/workspace-ops/project-docs.js +26 -15
- package/dist/workspace-ops/project-workspace.d.ts +12 -0
- package/dist/workspace-ops/project-workspace.js +257 -0
- package/dist/workspace-ops/reference-files.js +9 -9
- package/dist/workspace-ops/reference-summaries.js +9 -7
- package/dist/workspace-ops/system-commits.d.ts +7 -0
- package/dist/workspace-ops/system-commits.js +43 -0
- package/dist/workspace-ops/types.d.ts +22 -41
- package/migrations/README.md +2 -2
- package/package.json +1 -1
- package/prompts/README.md +2 -2
- package/prompts/prompt-flow-experiment-plan.md +19 -19
- package/prompts/prompt-flow-map.md +20 -19
- package/prompts/runs/README.md +5 -3
- package/prompts/runs/task-continuation.md +6 -4
- package/prompts/runs/task-retry.md +11 -10
- package/prompts/runs/task-run.md +6 -4
- package/web/assets/{homepage-motion-scene-DYV3iw6y.js → homepage-motion-scene-ENUzAyyp.js} +1 -1
- package/web/assets/{index-DNnf10zz.css → index-CWWkCr48.css} +1 -1
- package/web/assets/{index-D_bRz29f.js → index-DK8LBgHp.js} +3 -3
- package/web/index.html +2 -2
- package/dist/run-pipeline/promotion-reconcile.d.ts +0 -21
- package/dist/run-pipeline/promotion-reconcile.js +0 -195
- package/dist/workspace-ops/run-workspaces.d.ts +0 -19
- 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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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:
|
|
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
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
183
|
-
|
|
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
|
|
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
|
|
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
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
package/migrations/README.md
CHANGED
|
@@ -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
|
|
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
|
|
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
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
|
|
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 绝对路径、
|
|
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 ->
|
|
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
|
|
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`
|
|
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
|
|
33
|
-
-
|
|
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
|
-
-
|
|
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,
|
|
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
|
-
-
|
|
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
|
|
153
|
+
- No accepted commit or activity tag is created.
|
|
155
154
|
- Task remains explainable through Worklist and run result history.
|
|
156
155
|
|
|
157
|
-
### E6:
|
|
156
|
+
### E6: Persistent Workspace Dependency Continuity
|
|
158
157
|
|
|
159
158
|
Purpose:
|
|
160
159
|
|
|
161
|
-
- Verify
|
|
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
|
-
-
|
|
166
|
-
-
|
|
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
|
-
-
|
|
171
|
-
-
|
|
172
|
-
-
|
|
173
|
-
-
|
|
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
|
|