@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
package/web/index.html
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Newsreader:opsz,wght@6..72,600;6..72,700&display=swap"
|
|
14
14
|
/>
|
|
15
15
|
<title>Tutti</title>
|
|
16
|
-
<script type="module" crossorigin src="/assets/index-
|
|
17
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
16
|
+
<script type="module" crossorigin src="/assets/index-CxVBmEoi.js"></script>
|
|
17
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DNnf10zz.css">
|
|
18
18
|
</head>
|
|
19
19
|
<body>
|
|
20
20
|
<div id="root"></div>
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { CandidateArtifactValidationFailure } from "../artifacts/index.js";
|
|
2
|
-
import type { RunPipelineInput, RunPipelineResult } from "../control-plane/index.js";
|
|
3
|
-
import { type ResultCommit, type RunWorkspaceHandle } from "../workspace-ops/index.js";
|
|
4
|
-
import type { OpenAiRunPipelineRunnerOptions } from "./openai.js";
|
|
5
|
-
import type { CompletedNoRepoChangesTaskRunOutput, CompletedTaskRunOutput } from "./task-run-output.js";
|
|
6
|
-
export type PromotionReconcileOutcome = {
|
|
7
|
-
result: RunPipelineResult;
|
|
8
|
-
artifact_gate_failure?: {
|
|
9
|
-
correctable: boolean;
|
|
10
|
-
failure: CandidateArtifactValidationFailure;
|
|
11
|
-
};
|
|
12
|
-
};
|
|
13
|
-
export declare function runPromotionReconcile(input: {
|
|
14
|
-
options: OpenAiRunPipelineRunnerOptions;
|
|
15
|
-
pipelineInput: RunPipelineInput;
|
|
16
|
-
runHandle: RunWorkspaceHandle;
|
|
17
|
-
taskOutput: CompletedTaskRunOutput | CompletedNoRepoChangesTaskRunOutput;
|
|
18
|
-
sourceCommit: ResultCommit;
|
|
19
|
-
changedPaths: string[];
|
|
20
|
-
}): Promise<PromotionReconcileOutcome>;
|
|
21
|
-
//# sourceMappingURL=promotion-reconcile.d.ts.map
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import { dirname, join } from "node:path";
|
|
2
|
-
import { createNodeNpmCheckPlan, executeCheckPlan } from "../checks/index.js";
|
|
3
|
-
import { applyRunResultToMergeWorkspace, cleanupMergeWorkspace, collectMergeDiff, createMergeCandidateCommit, prepareMergeWorkspace, promoteMergeCandidate, WorkspaceOpsError, } from "../workspace-ops/index.js";
|
|
4
|
-
import { candidateChangedAfterChecks } from "./candidate-diff.js";
|
|
5
|
-
import { runArtifactCandidateGate } from "./artifact-applicability.js";
|
|
6
|
-
import { checkSkipFields, checkStatus, checksSummary, docsStatusForChangedPaths, failedBeforeChecks, failedFinishedResult, promotionNotPromoted, successfulRunSummary, } from "./result-projections.js";
|
|
7
|
-
function reconcileOutcome(result, artifactGateFailure) {
|
|
8
|
-
return {
|
|
9
|
-
result,
|
|
10
|
-
...(artifactGateFailure === undefined ? {} : { artifact_gate_failure: artifactGateFailure }),
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
function withPromotionReconcileEvent(result, event) {
|
|
14
|
-
return {
|
|
15
|
-
...result,
|
|
16
|
-
timeline_events: [...(result.timeline_events ?? []), event],
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
function promotionReconcileMeta(changedPaths) {
|
|
20
|
-
return [`changed paths: ${changedPaths.length}`];
|
|
21
|
-
}
|
|
22
|
-
function failedPromotionReconcileEvent(input) {
|
|
23
|
-
return {
|
|
24
|
-
event_kind: "promotion_reconcile",
|
|
25
|
-
status: "failed",
|
|
26
|
-
summary: input.summary,
|
|
27
|
-
meta: promotionReconcileMeta(input.changedPaths),
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
function mergeWorkspaceRootForOptions(options) {
|
|
31
|
-
return options.mergeWorkspaceRoot ?? join(dirname(options.runWorkspaceRoot), "merge-workspaces");
|
|
32
|
-
}
|
|
33
|
-
export async function runPromotionReconcile(input) {
|
|
34
|
-
let handle;
|
|
35
|
-
let changedPaths = input.changedPaths;
|
|
36
|
-
try {
|
|
37
|
-
handle = prepareMergeWorkspace({
|
|
38
|
-
workspaceRoot: input.options.workspaceRoot,
|
|
39
|
-
mergeWorkspaceRoot: mergeWorkspaceRootForOptions(input.options),
|
|
40
|
-
taskId: input.pipelineInput.task.id,
|
|
41
|
-
activityRef: input.pipelineInput.activity_ref,
|
|
42
|
-
sourceBaseRef: input.runHandle.base_ref,
|
|
43
|
-
sourceCommit: input.sourceCommit,
|
|
44
|
-
});
|
|
45
|
-
applyRunResultToMergeWorkspace(handle);
|
|
46
|
-
const diff = collectMergeDiff(handle);
|
|
47
|
-
changedPaths = diff.changed_paths;
|
|
48
|
-
if (changedPaths.length === 0) {
|
|
49
|
-
const summary = "Promotion reconcile produced no candidate changes.";
|
|
50
|
-
return reconcileOutcome(withPromotionReconcileEvent(failedBeforeChecks({
|
|
51
|
-
summary,
|
|
52
|
-
errorCode: "promotion_reconcile_no_candidate_changes",
|
|
53
|
-
retryable: false,
|
|
54
|
-
changedPaths,
|
|
55
|
-
promotion: promotionNotPromoted("promotion_reconcile_no_candidate_changes", "No reconciled candidate changes were available for promotion."),
|
|
56
|
-
}), failedPromotionReconcileEvent({ summary, changedPaths })));
|
|
57
|
-
}
|
|
58
|
-
input.pipelineInput.on_stage?.({
|
|
59
|
-
stage: "artifact_candidate_validate",
|
|
60
|
-
summary: "validating the reconciled candidate Artifact preview.",
|
|
61
|
-
});
|
|
62
|
-
const artifactGate = await runArtifactCandidateGate({
|
|
63
|
-
repoRoot: handle.repo_root,
|
|
64
|
-
task: {
|
|
65
|
-
title: input.pipelineInput.task.title,
|
|
66
|
-
goal: input.pipelineInput.task_detail.detail.contract.goal,
|
|
67
|
-
scope: input.pipelineInput.task_detail.detail.contract.scope,
|
|
68
|
-
},
|
|
69
|
-
diff,
|
|
70
|
-
config: input.options.config,
|
|
71
|
-
apiKey: input.options.apiKey,
|
|
72
|
-
...(input.options.promptsRoot === undefined
|
|
73
|
-
? {}
|
|
74
|
-
: { promptsRoot: input.options.promptsRoot }),
|
|
75
|
-
...(input.options.baseEnv === undefined ? {} : { baseEnv: input.options.baseEnv }),
|
|
76
|
-
...(input.options.artifactApplicabilityClient === undefined
|
|
77
|
-
? {}
|
|
78
|
-
: { client: input.options.artifactApplicabilityClient }),
|
|
79
|
-
...(input.options.artifactApplicabilityJudge === undefined
|
|
80
|
-
? {}
|
|
81
|
-
: { judge: input.options.artifactApplicabilityJudge }),
|
|
82
|
-
...(input.options.validateArtifactCandidate === undefined
|
|
83
|
-
? {}
|
|
84
|
-
: { validateCandidate: input.options.validateArtifactCandidate }),
|
|
85
|
-
...(input.options.recordUsage === undefined
|
|
86
|
-
? {}
|
|
87
|
-
: { recordUsage: input.options.recordUsage }),
|
|
88
|
-
activityRef: input.pipelineInput.activity_ref,
|
|
89
|
-
taskId: input.pipelineInput.task.id,
|
|
90
|
-
});
|
|
91
|
-
if (artifactGate.status === "failed") {
|
|
92
|
-
const summary = artifactGate.failure.summary;
|
|
93
|
-
const result = withPromotionReconcileEvent(failedBeforeChecks({
|
|
94
|
-
summary,
|
|
95
|
-
errorCode: artifactGate.failure.reason_code,
|
|
96
|
-
retryable: false,
|
|
97
|
-
changedPaths,
|
|
98
|
-
promotion: promotionNotPromoted(artifactGate.failure.reason_code, "Reconciled Artifact candidate validation failed, so promotion failed."),
|
|
99
|
-
}), failedPromotionReconcileEvent({ summary, changedPaths }));
|
|
100
|
-
return reconcileOutcome(result, {
|
|
101
|
-
correctable: artifactGate.correctable,
|
|
102
|
-
failure: artifactGate.failure,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
let docs = docsStatusForChangedPaths(changedPaths);
|
|
106
|
-
const checkPlan = createNodeNpmCheckPlan({ workspaceRoot: handle.repo_root });
|
|
107
|
-
const checks = await executeCheckPlan({
|
|
108
|
-
workspaceRoot: handle.repo_root,
|
|
109
|
-
plan: checkPlan,
|
|
110
|
-
...(input.options.checkTimeoutMs === undefined
|
|
111
|
-
? {}
|
|
112
|
-
: { timeoutMs: input.options.checkTimeoutMs }),
|
|
113
|
-
});
|
|
114
|
-
if (checks.status === "failed") {
|
|
115
|
-
const summary = checksSummary(checks);
|
|
116
|
-
return reconcileOutcome(withPromotionReconcileEvent(failedFinishedResult({
|
|
117
|
-
summary,
|
|
118
|
-
checks,
|
|
119
|
-
docs,
|
|
120
|
-
changedPaths,
|
|
121
|
-
promotion: promotionNotPromoted("promotion_reconcile_checks_failed", "Reconciled candidate checks failed, so promotion failed."),
|
|
122
|
-
}), failedPromotionReconcileEvent({ summary, changedPaths })));
|
|
123
|
-
}
|
|
124
|
-
const postChecksDiff = collectMergeDiff(handle);
|
|
125
|
-
changedPaths = postChecksDiff.changed_paths;
|
|
126
|
-
docs = docsStatusForChangedPaths(changedPaths);
|
|
127
|
-
if (candidateChangedAfterChecks(diff, postChecksDiff)) {
|
|
128
|
-
const summary = "Checks modified the reconciled candidate workspace; promotion failed.";
|
|
129
|
-
return reconcileOutcome(withPromotionReconcileEvent(failedFinishedResult({
|
|
130
|
-
summary,
|
|
131
|
-
checks: {
|
|
132
|
-
status: "failed",
|
|
133
|
-
commands: checks.commands,
|
|
134
|
-
},
|
|
135
|
-
docs,
|
|
136
|
-
changedPaths,
|
|
137
|
-
promotion: promotionNotPromoted("promotion_reconcile_checks_modified_candidate", "Checks changed tracked reconciled candidate files, so promotion failed."),
|
|
138
|
-
}), failedPromotionReconcileEvent({ summary, changedPaths })));
|
|
139
|
-
}
|
|
140
|
-
const commit = createMergeCandidateCommit(handle, `Reconcile Tutti task: ${input.pipelineInput.task.title}`);
|
|
141
|
-
const promotion = promoteMergeCandidate(handle, commit);
|
|
142
|
-
return reconcileOutcome(withPromotionReconcileEvent({
|
|
143
|
-
result_kind: "finished",
|
|
144
|
-
outcome: "completed",
|
|
145
|
-
summary: successfulRunSummary({
|
|
146
|
-
implementation: input.taskOutput.summary,
|
|
147
|
-
checks,
|
|
148
|
-
completion: "Promotion reconciled and complete.",
|
|
149
|
-
}),
|
|
150
|
-
...(input.taskOutput.user_note_candidate === undefined
|
|
151
|
-
? {}
|
|
152
|
-
: { user_note_candidate: input.taskOutput.user_note_candidate }),
|
|
153
|
-
checks: checkStatus(checks),
|
|
154
|
-
docs,
|
|
155
|
-
...checkSkipFields(checks),
|
|
156
|
-
changed_paths: changedPaths,
|
|
157
|
-
promotion: {
|
|
158
|
-
status: "promoted",
|
|
159
|
-
commit_oid: promotion.promoted_commit_oid,
|
|
160
|
-
summary: "Promoted reconciled candidate by fast-forward after checks.",
|
|
161
|
-
},
|
|
162
|
-
}, {
|
|
163
|
-
event_kind: "promotion_reconcile",
|
|
164
|
-
status: "completed",
|
|
165
|
-
summary: "Promotion reconciled and promoted.",
|
|
166
|
-
commit_oid: promotion.promoted_commit_oid,
|
|
167
|
-
meta: promotionReconcileMeta(changedPaths),
|
|
168
|
-
}));
|
|
169
|
-
}
|
|
170
|
-
catch (error) {
|
|
171
|
-
if (error instanceof WorkspaceOpsError) {
|
|
172
|
-
return reconcileOutcome(withPromotionReconcileEvent(failedBeforeChecks({
|
|
173
|
-
summary: error.message,
|
|
174
|
-
errorCode: `workspace_ops_${error.code}`,
|
|
175
|
-
retryable: error.code === "conflict",
|
|
176
|
-
changedPaths,
|
|
177
|
-
promotion: promotionNotPromoted("promotion_reconcile_failed", error.message),
|
|
178
|
-
}), failedPromotionReconcileEvent({ summary: error.message, changedPaths })));
|
|
179
|
-
}
|
|
180
|
-
const summary = error instanceof Error ? error.message : "Promotion reconcile failed";
|
|
181
|
-
return reconcileOutcome(withPromotionReconcileEvent(failedBeforeChecks({
|
|
182
|
-
summary,
|
|
183
|
-
errorCode: "promotion_reconcile_failed",
|
|
184
|
-
retryable: false,
|
|
185
|
-
changedPaths,
|
|
186
|
-
promotion: promotionNotPromoted("promotion_reconcile_failed", summary),
|
|
187
|
-
}), failedPromotionReconcileEvent({ summary, changedPaths })));
|
|
188
|
-
}
|
|
189
|
-
finally {
|
|
190
|
-
if (handle !== undefined) {
|
|
191
|
-
cleanupMergeWorkspace(handle);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
//# sourceMappingURL=promotion-reconcile.js.map
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { CleanupRunWorkspaceResult, CleanupStaleRunWorkspacesResult, MergeWorkspaceHandle, PrepareMergeWorkspaceOptions, PrepareRunWorkspaceOptions, PromotionResult, ResultCommit, RunDiffSummary, RunWorkspaceHandle } from "./types.js";
|
|
2
|
-
export declare function fingerprintWorkspaceCandidate(repoRoot: string): string;
|
|
3
|
-
export declare function prepareRunWorkspace(options: PrepareRunWorkspaceOptions): RunWorkspaceHandle;
|
|
4
|
-
export declare function collectRunDiff(handle: RunWorkspaceHandle): RunDiffSummary;
|
|
5
|
-
export declare function createResultCommit(handle: RunWorkspaceHandle, summary: string): ResultCommit;
|
|
6
|
-
export declare function prepareMergeWorkspace(options: PrepareMergeWorkspaceOptions): MergeWorkspaceHandle;
|
|
7
|
-
export declare function applyRunResultToMergeWorkspace(handle: MergeWorkspaceHandle): void;
|
|
8
|
-
export declare function collectMergeDiff(handle: MergeWorkspaceHandle): RunDiffSummary;
|
|
9
|
-
export declare function createMergeCandidateCommit(handle: MergeWorkspaceHandle, summary: string): ResultCommit;
|
|
10
|
-
export declare function promoteRunResult(handle: RunWorkspaceHandle, commit: ResultCommit): PromotionResult;
|
|
11
|
-
export declare function promoteMergeCandidate(handle: MergeWorkspaceHandle, commit: ResultCommit): PromotionResult;
|
|
12
|
-
export declare function cleanupRunWorkspace(handle: RunWorkspaceHandle): CleanupRunWorkspaceResult;
|
|
13
|
-
export declare function cleanupMergeWorkspace(handle: MergeWorkspaceHandle): CleanupRunWorkspaceResult;
|
|
14
|
-
export declare function cleanupStaleRunWorkspaces(options: {
|
|
15
|
-
workspaceRoot: string;
|
|
16
|
-
runWorkspaceRoot: string;
|
|
17
|
-
mergeWorkspaceRoot?: string;
|
|
18
|
-
}): CleanupStaleRunWorkspacesResult;
|
|
19
|
-
//# sourceMappingURL=run-workspaces.d.ts.map
|
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { classifyRepositoryPathMetadata } from "@tutti/shared/utils";
|
|
5
|
-
import { WorkspaceOpsError } from "./errors.js";
|
|
6
|
-
import { runGit, runGitRaw, runGitText, stagedChangesExist, summarizeGitFailure, tryGitText, } from "./git.js";
|
|
7
|
-
import { requireMainlineWriteReady } from "./mainline.js";
|
|
8
|
-
const MAX_RUN_DIFF_SUMMARY_BYTES = 32 * 1024;
|
|
9
|
-
export function fingerprintWorkspaceCandidate(repoRoot) {
|
|
10
|
-
const status = runGitRaw(["status", "--porcelain=v1", "-z"], repoRoot);
|
|
11
|
-
if (status.status !== 0) {
|
|
12
|
-
throw new WorkspaceOpsError("git_failed", summarizeGitFailure(["status"], status));
|
|
13
|
-
}
|
|
14
|
-
const diff = runGitRaw(["diff", "--binary", "HEAD", "--"], repoRoot);
|
|
15
|
-
if (diff.status !== 0) {
|
|
16
|
-
throw new WorkspaceOpsError("git_failed", summarizeGitFailure(["diff", "HEAD"], diff));
|
|
17
|
-
}
|
|
18
|
-
const untracked = runGitRaw(["ls-files", "--others", "--exclude-standard", "-z"], repoRoot);
|
|
19
|
-
if (untracked.status !== 0) {
|
|
20
|
-
throw new WorkspaceOpsError("git_failed", summarizeGitFailure(["ls-files", "--others"], untracked));
|
|
21
|
-
}
|
|
22
|
-
const hash = createHash("sha256").update(status.stdout).update(diff.stdout);
|
|
23
|
-
for (const path of untracked.stdout.toString("utf8").split("\0")) {
|
|
24
|
-
if (path.length === 0) {
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
const object = runGitRaw(["hash-object", "--no-filters", "--", path], repoRoot);
|
|
28
|
-
if (object.status !== 0) {
|
|
29
|
-
throw new WorkspaceOpsError("git_failed", summarizeGitFailure(["hash-object"], object));
|
|
30
|
-
}
|
|
31
|
-
hash.update(path).update("\0").update(object.stdout);
|
|
32
|
-
}
|
|
33
|
-
return hash.digest("hex");
|
|
34
|
-
}
|
|
35
|
-
function runWorkspaceTempBranch(taskId, activityRef) {
|
|
36
|
-
return `tutti/run/${taskId}/${activityRef}`;
|
|
37
|
-
}
|
|
38
|
-
function mergeWorkspaceTempBranch(taskId, activityRef) {
|
|
39
|
-
return `tutti/merge/${taskId}/${activityRef}`;
|
|
40
|
-
}
|
|
41
|
-
function truncateText(text, maxBytes) {
|
|
42
|
-
const buffer = Buffer.from(text, "utf8");
|
|
43
|
-
if (buffer.byteLength <= maxBytes) {
|
|
44
|
-
return text;
|
|
45
|
-
}
|
|
46
|
-
return `${buffer.subarray(0, maxBytes).toString("utf8")}\n[truncated]`;
|
|
47
|
-
}
|
|
48
|
-
function parsePorcelainChangedPaths(output) {
|
|
49
|
-
const entries = output
|
|
50
|
-
.toString("utf8")
|
|
51
|
-
.split("\0")
|
|
52
|
-
.filter((entry) => entry.length > 0);
|
|
53
|
-
const paths = new Set();
|
|
54
|
-
for (const entry of entries) {
|
|
55
|
-
const pathText = entry.slice(3);
|
|
56
|
-
const path = pathText.includes(" -> ") ? pathText.split(" -> ").at(-1) : pathText;
|
|
57
|
-
if (path === undefined) {
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
const classification = classifyRepositoryPathMetadata(path);
|
|
61
|
-
if (classification.kind === "allowed" && classification.path_kind === "project_relative") {
|
|
62
|
-
paths.add(classification.normalized_path);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return [...paths].sort((left, right) => left.localeCompare(right, "en"));
|
|
66
|
-
}
|
|
67
|
-
function collectWorkspaceDiff(repoRoot) {
|
|
68
|
-
const status = runGitRaw(["status", "--porcelain=v1", "-z"], repoRoot);
|
|
69
|
-
if (status.status !== 0) {
|
|
70
|
-
throw new WorkspaceOpsError("git_failed", summarizeGitFailure(["status"], status));
|
|
71
|
-
}
|
|
72
|
-
const changedPaths = parsePorcelainChangedPaths(status.stdout);
|
|
73
|
-
const summaryParts = [];
|
|
74
|
-
const statusText = runGitText(["status", "--short"], repoRoot);
|
|
75
|
-
if (statusText.length > 0) {
|
|
76
|
-
summaryParts.push(statusText);
|
|
77
|
-
}
|
|
78
|
-
const unstagedStat = tryGitText(["diff", "--stat"], repoRoot);
|
|
79
|
-
if (unstagedStat !== null && unstagedStat.length > 0) {
|
|
80
|
-
summaryParts.push(unstagedStat);
|
|
81
|
-
}
|
|
82
|
-
const stagedStat = tryGitText(["diff", "--cached", "--stat"], repoRoot);
|
|
83
|
-
if (stagedStat !== null && stagedStat.length > 0) {
|
|
84
|
-
summaryParts.push(stagedStat);
|
|
85
|
-
}
|
|
86
|
-
return {
|
|
87
|
-
changed_paths: changedPaths,
|
|
88
|
-
summary: truncateText(summaryParts.join("\n\n"), MAX_RUN_DIFF_SUMMARY_BYTES),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
export function prepareRunWorkspace(options) {
|
|
92
|
-
requireMainlineWriteReady(options.workspaceRoot);
|
|
93
|
-
const baseRef = runGitText(["rev-parse", "--verify", "HEAD"], options.workspaceRoot);
|
|
94
|
-
const tempBranch = runWorkspaceTempBranch(options.taskId, options.activityRef);
|
|
95
|
-
const runWorkspaceRoot = join(options.runWorkspaceRoot, options.activityRef);
|
|
96
|
-
const repoRoot = join(runWorkspaceRoot, "repo");
|
|
97
|
-
if (existsSync(runWorkspaceRoot)) {
|
|
98
|
-
throw new WorkspaceOpsError("conflict", "Run workspace already exists");
|
|
99
|
-
}
|
|
100
|
-
mkdirSync(options.runWorkspaceRoot, { recursive: true });
|
|
101
|
-
runGit(["worktree", "add", "-b", tempBranch, repoRoot, baseRef], options.workspaceRoot);
|
|
102
|
-
return {
|
|
103
|
-
activity_ref: options.activityRef,
|
|
104
|
-
task_id: options.taskId,
|
|
105
|
-
workspace_root: options.workspaceRoot,
|
|
106
|
-
run_workspace_root: runWorkspaceRoot,
|
|
107
|
-
repo_root: repoRoot,
|
|
108
|
-
base_ref: baseRef,
|
|
109
|
-
temp_branch: tempBranch,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
export function collectRunDiff(handle) {
|
|
113
|
-
return collectWorkspaceDiff(handle.repo_root);
|
|
114
|
-
}
|
|
115
|
-
function createCandidateCommit(input) {
|
|
116
|
-
runGit(["add", "-A"], input.repoRoot);
|
|
117
|
-
if (!stagedChangesExist(input.repoRoot)) {
|
|
118
|
-
throw new WorkspaceOpsError("validation_failed", input.emptyMessage);
|
|
119
|
-
}
|
|
120
|
-
runGit([
|
|
121
|
-
"commit",
|
|
122
|
-
"-m",
|
|
123
|
-
input.summary.trim().length === 0 ? "Complete Tutti task" : input.summary.trim(),
|
|
124
|
-
"-m",
|
|
125
|
-
input.metadata,
|
|
126
|
-
], input.repoRoot);
|
|
127
|
-
return {
|
|
128
|
-
commit_oid: runGitText(["rev-parse", "--verify", "HEAD"], input.repoRoot),
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
export function createResultCommit(handle, summary) {
|
|
132
|
-
return createCandidateCommit({
|
|
133
|
-
repoRoot: handle.repo_root,
|
|
134
|
-
summary,
|
|
135
|
-
emptyMessage: "Run result has no candidate changes",
|
|
136
|
-
metadata: `Task: ${handle.task_id}\nActivity: ${handle.activity_ref}\nBase: ${handle.base_ref}`,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
export function prepareMergeWorkspace(options) {
|
|
140
|
-
requireMainlineWriteReady(options.workspaceRoot);
|
|
141
|
-
const baseRef = runGitText(["rev-parse", "--verify", "HEAD"], options.workspaceRoot);
|
|
142
|
-
const tempBranch = mergeWorkspaceTempBranch(options.taskId, options.activityRef);
|
|
143
|
-
const mergeWorkspaceRoot = join(options.mergeWorkspaceRoot, options.activityRef);
|
|
144
|
-
const repoRoot = join(mergeWorkspaceRoot, "repo");
|
|
145
|
-
if (existsSync(mergeWorkspaceRoot)) {
|
|
146
|
-
throw new WorkspaceOpsError("conflict", "Merge workspace already exists");
|
|
147
|
-
}
|
|
148
|
-
mkdirSync(options.mergeWorkspaceRoot, { recursive: true });
|
|
149
|
-
runGit(["worktree", "add", "-b", tempBranch, repoRoot, baseRef], options.workspaceRoot);
|
|
150
|
-
return {
|
|
151
|
-
activity_ref: options.activityRef,
|
|
152
|
-
task_id: options.taskId,
|
|
153
|
-
workspace_root: options.workspaceRoot,
|
|
154
|
-
merge_workspace_root: mergeWorkspaceRoot,
|
|
155
|
-
repo_root: repoRoot,
|
|
156
|
-
base_ref: baseRef,
|
|
157
|
-
source_base_ref: options.sourceBaseRef,
|
|
158
|
-
source_result_commit_oid: options.sourceCommit.commit_oid,
|
|
159
|
-
temp_branch: tempBranch,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
export function applyRunResultToMergeWorkspace(handle) {
|
|
163
|
-
const result = runGitRaw(["cherry-pick", "--no-commit", handle.source_result_commit_oid], handle.repo_root);
|
|
164
|
-
if (result.status === 0) {
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
runGitRaw(["cherry-pick", "--abort"], handle.repo_root);
|
|
168
|
-
throw new WorkspaceOpsError("conflict", "Promotion reconcile could not apply the Run result to latest mainline", { reason_code: "promotion_reconcile_apply_failed" });
|
|
169
|
-
}
|
|
170
|
-
export function collectMergeDiff(handle) {
|
|
171
|
-
return collectWorkspaceDiff(handle.repo_root);
|
|
172
|
-
}
|
|
173
|
-
export function createMergeCandidateCommit(handle, summary) {
|
|
174
|
-
return createCandidateCommit({
|
|
175
|
-
repoRoot: handle.repo_root,
|
|
176
|
-
summary,
|
|
177
|
-
emptyMessage: "Promotion reconcile has no candidate changes",
|
|
178
|
-
metadata: [
|
|
179
|
-
`Task: ${handle.task_id}`,
|
|
180
|
-
`Activity: ${handle.activity_ref}`,
|
|
181
|
-
`Source-Base: ${handle.source_base_ref}`,
|
|
182
|
-
`Source-Commit: ${handle.source_result_commit_oid}`,
|
|
183
|
-
`Merge-Base: ${handle.base_ref}`,
|
|
184
|
-
].join("\n"),
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
export function promoteRunResult(handle, commit) {
|
|
188
|
-
requireMainlineWriteReady(handle.workspace_root);
|
|
189
|
-
const currentHead = runGitText(["rev-parse", "--verify", "HEAD"], handle.workspace_root);
|
|
190
|
-
if (currentHead !== handle.base_ref) {
|
|
191
|
-
throw new WorkspaceOpsError("conflict", "Mainline HEAD is no longer at the Run base ref", {
|
|
192
|
-
reason_code: "mainline_not_at_base",
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
runGit(["merge", "--ff-only", commit.commit_oid], handle.workspace_root);
|
|
196
|
-
return {
|
|
197
|
-
promoted_commit_oid: runGitText(["rev-parse", "--verify", "HEAD"], handle.workspace_root),
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
export function promoteMergeCandidate(handle, commit) {
|
|
201
|
-
requireMainlineWriteReady(handle.workspace_root);
|
|
202
|
-
const currentHead = runGitText(["rev-parse", "--verify", "HEAD"], handle.workspace_root);
|
|
203
|
-
if (currentHead !== handle.base_ref) {
|
|
204
|
-
throw new WorkspaceOpsError("conflict", "Mainline HEAD changed while promotion reconcile was preparing a candidate", { reason_code: "promotion_reconcile_mainline_moved" });
|
|
205
|
-
}
|
|
206
|
-
runGit(["merge", "--ff-only", commit.commit_oid], handle.workspace_root);
|
|
207
|
-
return {
|
|
208
|
-
promoted_commit_oid: runGitText(["rev-parse", "--verify", "HEAD"], handle.workspace_root),
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
function cleanupWorktree(input) {
|
|
212
|
-
const warnings = [];
|
|
213
|
-
if (existsSync(input.repoRoot)) {
|
|
214
|
-
let remove = runGitRaw(["worktree", "remove", "--force", input.repoRoot], input.workspaceRoot);
|
|
215
|
-
if (remove.status !== 0) {
|
|
216
|
-
remove = runGitRaw(["worktree", "remove", "--force", "--force", input.repoRoot], input.workspaceRoot);
|
|
217
|
-
}
|
|
218
|
-
if (remove.status !== 0) {
|
|
219
|
-
warnings.push(summarizeGitFailure(["worktree", "remove"], remove));
|
|
220
|
-
rmSync(input.workspaceDir, { recursive: true, force: true });
|
|
221
|
-
runGitRaw(["worktree", "prune"], input.workspaceRoot);
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
rmSync(input.workspaceDir, { recursive: true, force: true });
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
else {
|
|
228
|
-
rmSync(input.workspaceDir, { recursive: true, force: true });
|
|
229
|
-
}
|
|
230
|
-
const deleteBranch = runGitRaw(["branch", "-D", input.tempBranch], input.workspaceRoot);
|
|
231
|
-
if (deleteBranch.status !== 0) {
|
|
232
|
-
warnings.push(summarizeGitFailure(["branch", "-D", input.tempBranch], deleteBranch));
|
|
233
|
-
}
|
|
234
|
-
return { warnings };
|
|
235
|
-
}
|
|
236
|
-
export function cleanupRunWorkspace(handle) {
|
|
237
|
-
return cleanupWorktree({
|
|
238
|
-
workspaceRoot: handle.workspace_root,
|
|
239
|
-
repoRoot: handle.repo_root,
|
|
240
|
-
workspaceDir: handle.run_workspace_root,
|
|
241
|
-
tempBranch: handle.temp_branch,
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
export function cleanupMergeWorkspace(handle) {
|
|
245
|
-
return cleanupWorktree({
|
|
246
|
-
workspaceRoot: handle.workspace_root,
|
|
247
|
-
repoRoot: handle.repo_root,
|
|
248
|
-
workspaceDir: handle.merge_workspace_root,
|
|
249
|
-
tempBranch: handle.temp_branch,
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
export function cleanupStaleRunWorkspaces(options) {
|
|
253
|
-
const warnings = [];
|
|
254
|
-
let removedRunWorkspaceCount = 0;
|
|
255
|
-
for (const workspaceRoot of [options.runWorkspaceRoot, options.mergeWorkspaceRoot]) {
|
|
256
|
-
if (workspaceRoot === undefined || !existsSync(workspaceRoot)) {
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
try {
|
|
260
|
-
for (const entry of readdirSync(workspaceRoot, { withFileTypes: true })) {
|
|
261
|
-
rmSync(join(workspaceRoot, entry.name), { recursive: true, force: true });
|
|
262
|
-
removedRunWorkspaceCount += 1;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
catch {
|
|
266
|
-
warnings.push("Failed to remove one or more stale run workspaces.");
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
const prune = runGitRaw(["worktree", "prune"], options.workspaceRoot);
|
|
270
|
-
if (prune.status !== 0) {
|
|
271
|
-
warnings.push("Failed to prune stale Git worktree metadata.");
|
|
272
|
-
}
|
|
273
|
-
let removedTempBranchCount = 0;
|
|
274
|
-
const branchPatterns = options.mergeWorkspaceRoot === undefined ? ["tutti/run/*"] : ["tutti/run/*", "tutti/merge/*"];
|
|
275
|
-
for (const pattern of branchPatterns) {
|
|
276
|
-
const branches = runGitRaw(["branch", "--list", pattern, "--format=%(refname:short)"], options.workspaceRoot);
|
|
277
|
-
if (branches.status !== 0) {
|
|
278
|
-
warnings.push(`Failed to list stale ${pattern} branches.`);
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
const branchNames = branches.stdout
|
|
282
|
-
.toString("utf8")
|
|
283
|
-
.split("\n")
|
|
284
|
-
.map((line) => line.trim())
|
|
285
|
-
.filter((line) => line.length > 0);
|
|
286
|
-
for (const branchName of branchNames) {
|
|
287
|
-
const deleted = runGitRaw(["branch", "-D", branchName], options.workspaceRoot);
|
|
288
|
-
if (deleted.status === 0) {
|
|
289
|
-
removedTempBranchCount += 1;
|
|
290
|
-
}
|
|
291
|
-
else {
|
|
292
|
-
warnings.push(`Failed to delete stale run branch ${branchName}.`);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return {
|
|
297
|
-
removed_run_workspace_count: removedRunWorkspaceCount,
|
|
298
|
-
removed_temp_branch_count: removedTempBranchCount,
|
|
299
|
-
warnings,
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
//# sourceMappingURL=run-workspaces.js.map
|