@xfey/tutti 0.1.49 → 0.1.51
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/package.json +1 -1
- package/prompts/README.md +2 -2
- package/prompts/prompt-flow-experiment-plan.md +5 -6
- 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-CKLsNPVv.js → homepage-motion-scene-CV9ceFzS.js} +1 -1
- package/web/assets/index-CxVBmEoi.js +70 -0
- package/web/assets/{index-DQogRzu_.css → index-DNnf10zz.css} +1 -1
- 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
- package/web/assets/index-CkXOC6nC.js +0 -69
|
@@ -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/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:
|
package/prompts/runs/README.md
CHANGED
|
@@ -4,7 +4,7 @@ These templates are used by task `Run` execution.
|
|
|
4
4
|
|
|
5
5
|
- `task-run.md` is rendered for initial task Runs.
|
|
6
6
|
- `task-continuation.md` is rendered for Phase 8 continuation Runs after task-bound clarification.
|
|
7
|
-
- `task-retry.md` is rendered for bounded Run pipeline self-correction retries after
|
|
7
|
+
- `task-retry.md` is rendered for bounded Run pipeline self-correction retries after feedback such as no candidate diff, Artifact validation failure, failed checks, source mutation, or accepted-checkpoint failure. It is not a failed-task follow-up path and does not resurrect `Task.failed`.
|
|
8
8
|
Templates in this directory may describe task contracts, repo evidence, check expectations, artifact declaration rules, and documentation update responsibilities. They must never include provider credentials or host-local secret material.
|
|
9
9
|
|
|
10
10
|
`task-run.md` allows `needs_human` only for human requirement decisions, non-secret context, or risk confirmation. Unusable candidates, ordinary missing diffs, or repo/tooling errors that prevent implementation should be returned as `failed` so the Run does not open task-bound clarification incorrectly. If the task contract does not require repository file changes, the output can use `completed_no_repo_changes`; otherwise a completed implementation is expected to produce a candidate diff. If the candidate is complete but Codex self-validation commands are unavailable or blocked, the task-run output should still be `completed`; Tutti checks are the authoritative validation gate.
|
|
@@ -13,6 +13,8 @@ All three Run templates use a slim mutually exclusive structured output schema u
|
|
|
13
13
|
Run templates describe `docs/reference/` as a user-visible file exchange area. Human uploads remain under `docs/reference/files/` and `docs/reference/images/`; `docs/reference/tutti/**` is for Tutti-generated files intended for direct member review in References, not temporary output, internal notes, logs, or canonical project documentation. Generated user-visible files must be written under `docs/reference/tutti/`, with `docs/reference/tutti/<meaningful-folder>/...` used when the folder name should be shown as a References UI group. Generated files under `docs/reference/tutti/` are discovered from promoted changed paths, rendered by References automatically, and may be listed in completed task result cards.
|
|
14
14
|
Initial `task-run.md`, active `task-continuation.md`, and active pipeline `task-retry.md` also instruct Codex to write or update the single active `tutti.artifact.json` manifest only when the task creates or updates a member-viewable visual artifact. Each template includes the complete strict static and server JSON shapes: every behavior field is required, `network` is always an explicit boolean, and server `script / ready_path / port` fields are forbidden. A server package instead declares the fixed `artifact:preview` npm script in its own `package.json` and listens on the injected `HOST / PORT`. The manifest remains repo truth and is not reported through the structured output schema.
|
|
15
15
|
Active task-bound follow-up still only starts continuation Runs from `Run.needs_human`. Pipeline self-correction retry happens before a terminal Run result is recorded and does not open clarification.
|
|
16
|
-
|
|
16
|
+
All three templates state that cwd is the persistent project workspace, may contain unaccepted source and project-local runtime/dependency state from earlier failed attempts, and must not be reset, checked out, stashed, cleaned, or otherwise discarded. Continuation, provider transient retry, and self-correction reuse that same directory and accumulated state.
|
|
17
|
+
|
|
18
|
+
Provider transient retry is separate from self-correction. If a retryable Codex app-server workspace-write failure occurs before usable structured output, Run pipeline may rerun the same prompt family once in the same project workspace: initial Runs stay on `task-run.md`, continuation Runs stay on `task-continuation.md`, and self-correction attempts stay on `task-retry.md`. This retry does not add `RunCorrectionContext` unless the failed attempt was already a self-correction attempt.
|
|
17
19
|
Run templates are responsible for telling Codex to update related project documentation in the same candidate workspace whenever implementation changes stable project facts. Broader cleanup, fact reconciliation, and phase-level documentation maintenance belong to the independent `context_sync` Procedure.
|
|
18
|
-
|
|
20
|
+
Successful source-changing Runs create an accepted commit and immutable `tutti/accepted/<activity_ref>` tag after Artifact validation and checks. Prompt templates do not own staging, commit/tag creation, recovery, or promotion semantics.
|
|
@@ -6,9 +6,9 @@ model_path: codex.app_server.workspace_write
|
|
|
6
6
|
|
|
7
7
|
# Role
|
|
8
8
|
|
|
9
|
-
You continue one formal Tutti task after task-bound clarification, inside
|
|
9
|
+
You continue one formal Tutti task after task-bound clarification, inside the project's persistent workspace.
|
|
10
10
|
|
|
11
|
-
Treat this as continuation of the same frozen task contract, not a new task and not a scope change. Return only the structured JSON result; Tutti will inspect the Git diff and, when repository candidate changes exist, run checks
|
|
11
|
+
Treat this as continuation of the same frozen task contract, not a new task and not a scope change. Return only the structured JSON result; Tutti will inspect the Git diff and, when repository candidate changes exist, run checks and create an accepted checkpoint for successful changes.
|
|
12
12
|
|
|
13
13
|
# Inputs
|
|
14
14
|
|
|
@@ -42,7 +42,9 @@ Human clarification answers may clarify or confirm work inside the frozen task c
|
|
|
42
42
|
- Use `title` only for orientation; it does not expand the contract.
|
|
43
43
|
- Use clarification answers only within the frozen contract.
|
|
44
44
|
- Do not use main chat or Scratchpad as hidden scope.
|
|
45
|
-
- Work only inside the provided
|
|
45
|
+
- Work only inside the provided project workspace.
|
|
46
|
+
- This workspace is the persistent development state. It may retain unaccepted source changes and project-local dependency or runtime state from the paused or failed work; inspect and continue from that state within the frozen task contract.
|
|
47
|
+
- Do not reset, checkout, stash, clean, or otherwise discard existing workspace state. Do not remove changes merely because they were created before this continuation.
|
|
46
48
|
- Prefer small, reviewable changes that preserve existing style and tests.
|
|
47
49
|
|
|
48
50
|
If the formal task contract plus submitted clarification is still insufficient, return `needs_human` only for a human requirement decision, non-secret context, or risk confirmation. Return `failed` when no usable candidate can be produced.
|
|
@@ -68,7 +70,7 @@ When this task explicitly produces such a file deliverable, write it under `docs
|
|
|
68
70
|
|
|
69
71
|
# Documentation
|
|
70
72
|
|
|
71
|
-
If this change affects stable project facts, usage, public behavior, interfaces, setup, project organization, maintenance context, or project working rules, update the relevant project documentation in the same
|
|
73
|
+
If this change affects stable project facts, usage, public behavior, interfaces, setup, project organization, maintenance context, or project working rules, update the relevant project documentation in the same project workspace.
|
|
72
74
|
|
|
73
75
|
Broader phase-level documentation cleanup belongs to Tutti `context_sync`, but do not defer documentation that is directly required to make this task result understandable.
|
|
74
76
|
|