@abelxiaoxing/cadence 1.0.2 → 1.0.3
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/agents/contract-reviewer.md +2 -2
- package/agents/diagnosis-worker.md +3 -2
- package/agents/implementation-worker.md +3 -2
- package/package.json +1 -1
- package/prompts/abel-design.md +13 -3
- package/prompts/abel-implement.md +50 -21
- package/skills/abel-workflow/SKILL.md +57 -25
- package/src/candidate-preflight.ts +361 -56
- package/src/child-session.ts +92 -27
- package/src/contracts.ts +987 -38
- package/src/index.ts +50 -24
- package/src/parent-provider.ts +89 -23
- package/src/patch.ts +269 -41
- package/src/result-store.ts +254 -16
- package/src/runtime.ts +1281 -560
- package/src/scheduler.ts +59 -16
- package/src/subagent-activity.ts +56 -25
- package/src/submit-tool.ts +12 -2
- package/src/worker.ts +114 -195
package/src/index.ts
CHANGED
|
@@ -99,6 +99,14 @@ function activateDispatcher(
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
function splitUsage(result: unknown): { payload: unknown; usage?: unknown } {
|
|
103
|
+
if (typeof result !== "object" || result === null || !("usage" in result)) {
|
|
104
|
+
return { payload: result };
|
|
105
|
+
}
|
|
106
|
+
const { usage, ...payload } = result as Record<string, unknown>;
|
|
107
|
+
return { payload, usage };
|
|
108
|
+
}
|
|
109
|
+
|
|
102
110
|
export default function register(pi: ExtensionAPI): void {
|
|
103
111
|
const parentPayloadBridge = new ParentPayloadBridge();
|
|
104
112
|
const runtime = new Runtime({ parentPayloadBridge });
|
|
@@ -108,7 +116,7 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
108
116
|
name: DISPATCH_TOOL,
|
|
109
117
|
label: "Abel Dispatch",
|
|
110
118
|
description:
|
|
111
|
-
"Private Abel workflow delegation: run bounded read-only evidence or Worker phase requests, apply or discard retained results, cancel work, or finish the stage. Inactive unless an eligible Abel stage verified its invocation.",
|
|
119
|
+
"Private Abel workflow delegation: run bounded read-only evidence or Worker phase requests, apply or discard retained results, apply a parent-only stable AGENTS checkpoint, cancel work, or finish the stage. Inactive unless an eligible Abel stage verified its invocation.",
|
|
112
120
|
parameters: {
|
|
113
121
|
type: "object",
|
|
114
122
|
properties: {
|
|
@@ -121,12 +129,32 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
121
129
|
type: "string",
|
|
122
130
|
description: "Retained result id for apply/discard",
|
|
123
131
|
},
|
|
132
|
+
requestId: {
|
|
133
|
+
type: "string",
|
|
134
|
+
description: "Current Implement apply/discard operation identity",
|
|
135
|
+
},
|
|
136
|
+
rejection: {
|
|
137
|
+
type: "object",
|
|
138
|
+
description: "Typed parent rejection for Implement discard",
|
|
139
|
+
},
|
|
140
|
+
agentsCheckpoint: {
|
|
141
|
+
type: "object",
|
|
142
|
+
description:
|
|
143
|
+
"Parent-owned approved managed-block checkpoint for action=apply (mutually exclusive with resultId)",
|
|
144
|
+
},
|
|
124
145
|
},
|
|
125
146
|
required: ["action"],
|
|
126
147
|
},
|
|
127
148
|
async execute(
|
|
128
149
|
toolCallId: string,
|
|
129
|
-
params: {
|
|
150
|
+
params: {
|
|
151
|
+
action?: string;
|
|
152
|
+
request?: unknown;
|
|
153
|
+
resultId?: string;
|
|
154
|
+
requestId?: string;
|
|
155
|
+
rejection?: unknown;
|
|
156
|
+
agentsCheckpoint?: unknown;
|
|
157
|
+
},
|
|
130
158
|
signal: AbortSignal | undefined,
|
|
131
159
|
onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
|
132
160
|
ctx: ExtensionContext,
|
|
@@ -135,10 +163,11 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
135
163
|
const validRun =
|
|
136
164
|
action === "run" && runtime.validateRequest(params.request).ok;
|
|
137
165
|
const tuiRun = ctx.mode === "tui" && validRun;
|
|
166
|
+
const { action: _action, ...operation } = params;
|
|
138
167
|
const result = tuiRun
|
|
139
168
|
? await runtime.execute(
|
|
140
169
|
action,
|
|
141
|
-
|
|
170
|
+
operation,
|
|
142
171
|
ctx,
|
|
143
172
|
signal,
|
|
144
173
|
activity.observe(
|
|
@@ -146,24 +175,21 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
146
175
|
onUpdate as ((result: unknown) => void) | undefined,
|
|
147
176
|
),
|
|
148
177
|
)
|
|
149
|
-
: await runtime.execute(action,
|
|
178
|
+
: await runtime.execute(action, operation, ctx, signal);
|
|
150
179
|
const display = tuiRun
|
|
151
180
|
? activity.finalize(toolCallId, result)
|
|
152
181
|
: undefined;
|
|
182
|
+
const { payload, usage } = splitUsage(result);
|
|
153
183
|
const details = display
|
|
154
|
-
? {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
details,
|
|
160
|
-
usage: result.usage,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
184
|
+
? {
|
|
185
|
+
...(payload as Record<string, unknown>),
|
|
186
|
+
[ACTIVITY_DETAILS_KEY]: display,
|
|
187
|
+
}
|
|
188
|
+
: payload;
|
|
163
189
|
return {
|
|
164
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
190
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
165
191
|
details,
|
|
166
|
-
|
|
192
|
+
...(usage === undefined ? {} : { usage }),
|
|
167
193
|
};
|
|
168
194
|
},
|
|
169
195
|
renderCall(args: unknown, theme: Theme, _context: unknown) {
|
|
@@ -207,7 +233,15 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
207
233
|
activateDispatcher(pi, runtime.activation, prompt, event.prompt);
|
|
208
234
|
});
|
|
209
235
|
|
|
210
|
-
pi.on("session_start", (_event, ctx) => {
|
|
236
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
237
|
+
pendingPrompt = undefined;
|
|
238
|
+
activity.detach();
|
|
239
|
+
const active = pi.getActiveTools();
|
|
240
|
+
if (active.includes(DISPATCH_TOOL)) {
|
|
241
|
+
pi.setActiveTools(deactivateTool(active, DISPATCH_TOOL));
|
|
242
|
+
}
|
|
243
|
+
await runtime.drain();
|
|
244
|
+
activity.clear();
|
|
211
245
|
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
212
246
|
if (typeof sessionId === "string") {
|
|
213
247
|
parentPayloadBridge.beginSession(sessionId);
|
|
@@ -217,15 +251,7 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
217
251
|
} else {
|
|
218
252
|
parentPayloadBridge.clear();
|
|
219
253
|
}
|
|
220
|
-
pendingPrompt = undefined;
|
|
221
|
-
activity.detach();
|
|
222
254
|
if (ctx.mode === "tui") activity.attach(ctx.ui);
|
|
223
|
-
// Restore the default: dispatch is inactive unless a verified stage
|
|
224
|
-
// reactivates it through the activation helpers.
|
|
225
|
-
const active = pi.getActiveTools();
|
|
226
|
-
if (active.includes(DISPATCH_TOOL)) {
|
|
227
|
-
pi.setActiveTools(deactivateTool(active, DISPATCH_TOOL));
|
|
228
|
-
}
|
|
229
255
|
});
|
|
230
256
|
|
|
231
257
|
pi.on("model_select", (event, ctx) => {
|
package/src/parent-provider.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type ExtensionContext,
|
|
8
8
|
ModelRuntime,
|
|
9
9
|
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import type { ChildFailure } from "./contracts.ts";
|
|
10
11
|
import type {
|
|
11
12
|
ParentPayloadBridge,
|
|
12
13
|
ParentPayloadCallback,
|
|
@@ -18,6 +19,33 @@ export interface PhasePayloadBridge {
|
|
|
18
19
|
readonly capture: ParentPayloadCapture;
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
type PhaseRuntimeFailure = Extract<
|
|
23
|
+
ChildFailure,
|
|
24
|
+
{ kind: "cancelled" | "environment" | "transport" }
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
export type PhaseRuntimeResult =
|
|
28
|
+
| { ok: true; modelRuntime: ModelRuntime; model: Model<string> }
|
|
29
|
+
| { ok: false; error: string; failure: PhaseRuntimeFailure };
|
|
30
|
+
|
|
31
|
+
function phaseRuntimeFailure(
|
|
32
|
+
failure: PhaseRuntimeFailure,
|
|
33
|
+
error: string,
|
|
34
|
+
): Extract<PhaseRuntimeResult, { ok: false }> {
|
|
35
|
+
return { ok: false, error, failure };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function cancelledPhaseRuntime(): Extract<PhaseRuntimeResult, { ok: false }> {
|
|
39
|
+
return phaseRuntimeFailure(
|
|
40
|
+
{ kind: "cancelled", code: "cancelled" },
|
|
41
|
+
"child phase cancelled",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isCancellation(error: unknown, signal?: AbortSignal): boolean {
|
|
46
|
+
return signal?.aborted === true && error === signal.reason;
|
|
47
|
+
}
|
|
48
|
+
|
|
21
49
|
function phasePayloadCallback(
|
|
22
50
|
payloadBridge: PhasePayloadBridge,
|
|
23
51
|
childOnPayload: ParentPayloadCallback | undefined,
|
|
@@ -137,44 +165,82 @@ export async function runtimeFromContext(
|
|
|
137
165
|
ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
|
|
138
166
|
payloadBridge: ParentPayloadBridge,
|
|
139
167
|
signal?: AbortSignal,
|
|
140
|
-
): Promise<
|
|
141
|
-
signal?.
|
|
142
|
-
if (!ctx.model)
|
|
168
|
+
): Promise<PhaseRuntimeResult> {
|
|
169
|
+
if (signal?.aborted) return cancelledPhaseRuntime();
|
|
170
|
+
if (!ctx.model) {
|
|
171
|
+
return phaseRuntimeFailure(
|
|
172
|
+
{ kind: "environment", code: "sandbox-runtime-unavailable" },
|
|
173
|
+
"parent model is unavailable",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
143
176
|
const selectedModel = ctx.model;
|
|
144
177
|
const selectedModelKey = modelKeyFor(selectedModel);
|
|
145
|
-
|
|
146
|
-
ctx.modelRegistry.getApiKeyAndHeaders
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
178
|
+
let resolved: Awaited<
|
|
179
|
+
ReturnType<typeof ctx.modelRegistry.getApiKeyAndHeaders>
|
|
180
|
+
>;
|
|
181
|
+
try {
|
|
182
|
+
resolved = await abortable(
|
|
183
|
+
ctx.modelRegistry.getApiKeyAndHeaders(selectedModel),
|
|
184
|
+
signal,
|
|
185
|
+
);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (isCancellation(error, signal)) return cancelledPhaseRuntime();
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
if (!resolved.ok) {
|
|
191
|
+
return phaseRuntimeFailure(
|
|
192
|
+
{ kind: "environment", code: "sandbox-runtime-unavailable" },
|
|
193
|
+
"phase authentication is unavailable",
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (signal?.aborted) return cancelledPhaseRuntime();
|
|
151
197
|
const model = {
|
|
152
198
|
...selectedModel,
|
|
153
199
|
baseUrl: resolved.baseUrl ?? selectedModel.baseUrl,
|
|
154
200
|
} as Model<string>;
|
|
155
201
|
const effectiveModelKey = modelKeyFor(model);
|
|
156
202
|
const capture = payloadBridge.capture(effectiveModelKey, ctx.modelRegistry);
|
|
157
|
-
if (!capture)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
203
|
+
if (!capture) {
|
|
204
|
+
return phaseRuntimeFailure(
|
|
205
|
+
{ kind: "transport", code: "transport-failure" },
|
|
206
|
+
"parent payload bridge is unavailable",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
const captureIsReady = () => {
|
|
210
|
+
const currentModel = ctx.model;
|
|
211
|
+
return (
|
|
212
|
+
currentModel !== undefined &&
|
|
213
|
+
sameSelectedModel(currentModel, selectedModelKey) &&
|
|
214
|
+
payloadBridge.capture(effectiveModelKey, ctx.modelRegistry) === capture
|
|
215
|
+
);
|
|
166
216
|
};
|
|
167
|
-
|
|
217
|
+
if (!captureIsReady()) {
|
|
218
|
+
return phaseRuntimeFailure(
|
|
219
|
+
{ kind: "transport", code: "transport-failure" },
|
|
220
|
+
"parent payload bridge is unavailable",
|
|
221
|
+
);
|
|
222
|
+
}
|
|
168
223
|
const delegate = capture.delegate;
|
|
169
224
|
if (!delegate) throw new Error("parent Provider is unavailable");
|
|
170
225
|
const provider = phaseProvider(delegate, resolved, {
|
|
171
226
|
bridge: payloadBridge,
|
|
172
227
|
capture,
|
|
173
228
|
});
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
229
|
+
let modelRuntime: ModelRuntime;
|
|
230
|
+
try {
|
|
231
|
+
modelRuntime = await runtimeForProvider(provider, signal);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (isCancellation(error, signal)) return cancelledPhaseRuntime();
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
if (signal?.aborted) return cancelledPhaseRuntime();
|
|
237
|
+
if (!captureIsReady()) {
|
|
238
|
+
return phaseRuntimeFailure(
|
|
239
|
+
{ kind: "transport", code: "transport-failure" },
|
|
240
|
+
"parent payload bridge is unavailable",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
return { ok: true, modelRuntime, model };
|
|
178
244
|
}
|
|
179
245
|
|
|
180
246
|
function abortable<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
|
package/src/patch.ts
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
copyFileSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
2
12
|
import { preflightCandidate } from "./candidate-preflight.ts";
|
|
3
|
-
import {
|
|
4
|
-
|
|
13
|
+
import {
|
|
14
|
+
type AgentsCheckpointRequest,
|
|
15
|
+
type ApplyCandidateResult,
|
|
16
|
+
type CandidateFailure,
|
|
17
|
+
diffWritePaths,
|
|
18
|
+
validateAgentsCheckpointRequest,
|
|
19
|
+
} from "./contracts.ts";
|
|
20
|
+
import { type Bound, isCurrent } from "./file-snapshot.ts";
|
|
5
21
|
import type { ResultStore } from "./result-store.ts";
|
|
6
22
|
|
|
7
23
|
interface CommandResult {
|
|
@@ -58,8 +74,223 @@ async function git(
|
|
|
58
74
|
});
|
|
59
75
|
}
|
|
60
76
|
|
|
61
|
-
function
|
|
62
|
-
|
|
77
|
+
function failure(value: CandidateFailure): {
|
|
78
|
+
ok: false;
|
|
79
|
+
failure: CandidateFailure;
|
|
80
|
+
} {
|
|
81
|
+
return { ok: false, failure: value };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function cancellationFailure(): {
|
|
85
|
+
ok: false;
|
|
86
|
+
failure: CandidateFailure;
|
|
87
|
+
} {
|
|
88
|
+
return failure({ kind: "cancelled", code: "cancelled" });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type AgentsCheckpointResult =
|
|
92
|
+
| {
|
|
93
|
+
ok: true;
|
|
94
|
+
target: string;
|
|
95
|
+
agentsImpact: AgentsCheckpointRequest["agentsImpact"];
|
|
96
|
+
checkExitCode: 0;
|
|
97
|
+
applyExitCode: 0;
|
|
98
|
+
}
|
|
99
|
+
| {
|
|
100
|
+
ok: false;
|
|
101
|
+
failure: CandidateFailure;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const AGENTS_START = "<!-- ABEL:AGENTS-INDEX:START -->";
|
|
105
|
+
const AGENTS_END = "<!-- ABEL:AGENTS-INDEX:END -->";
|
|
106
|
+
const NONREGULAR_AGENTS_TARGET = Symbol("nonregular AGENTS target");
|
|
107
|
+
|
|
108
|
+
function managedParts(text: string): { prefix: string; suffix: string } | null {
|
|
109
|
+
const lines = text.split("\n");
|
|
110
|
+
const starts: Array<{ line: number; start: number; end: number }> = [];
|
|
111
|
+
const ends: Array<{ line: number; start: number; end: number }> = [];
|
|
112
|
+
let offset = 0;
|
|
113
|
+
for (const [line, value] of lines.entries()) {
|
|
114
|
+
const range = { line, start: offset, end: offset + value.length };
|
|
115
|
+
if (value === AGENTS_START) starts.push(range);
|
|
116
|
+
if (value === AGENTS_END) ends.push(range);
|
|
117
|
+
offset = range.end + 1;
|
|
118
|
+
}
|
|
119
|
+
if (
|
|
120
|
+
starts.length !== 1 ||
|
|
121
|
+
ends.length !== 1 ||
|
|
122
|
+
starts[0].line >= ends[0].line
|
|
123
|
+
) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
prefix: text.slice(0, starts[0].start),
|
|
128
|
+
suffix: text.slice(ends[0].end),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function checkpointContentIsScoped(
|
|
133
|
+
impact: AgentsCheckpointRequest["agentsImpact"],
|
|
134
|
+
before: string | null,
|
|
135
|
+
after: string | null,
|
|
136
|
+
): boolean {
|
|
137
|
+
const beforeParts = before === null ? null : managedParts(before);
|
|
138
|
+
const afterParts = after === null ? null : managedParts(after);
|
|
139
|
+
if (impact === "update-existing") {
|
|
140
|
+
return (
|
|
141
|
+
beforeParts !== null &&
|
|
142
|
+
afterParts !== null &&
|
|
143
|
+
beforeParts.prefix === afterParts.prefix &&
|
|
144
|
+
beforeParts.suffix === afterParts.suffix
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (impact === "create-index") {
|
|
148
|
+
return (
|
|
149
|
+
before === null &&
|
|
150
|
+
afterParts !== null &&
|
|
151
|
+
afterParts.prefix.trim() === "" &&
|
|
152
|
+
afterParts.suffix.trim() === ""
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (beforeParts === null) return false;
|
|
156
|
+
if (after === null) {
|
|
157
|
+
return beforeParts.prefix.trim() === "" && beforeParts.suffix.trim() === "";
|
|
158
|
+
}
|
|
159
|
+
return (
|
|
160
|
+
managedParts(after) === null &&
|
|
161
|
+
after === `${beforeParts.prefix}${beforeParts.suffix}`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function regularText(
|
|
166
|
+
root: string,
|
|
167
|
+
target: string,
|
|
168
|
+
): string | null | typeof NONREGULAR_AGENTS_TARGET {
|
|
169
|
+
const resolvedRoot = path.resolve(root);
|
|
170
|
+
const absolute = path.resolve(resolvedRoot, target);
|
|
171
|
+
if (
|
|
172
|
+
absolute === resolvedRoot ||
|
|
173
|
+
!absolute.startsWith(`${resolvedRoot}${path.sep}`)
|
|
174
|
+
) {
|
|
175
|
+
throw new Error("AGENTS target escapes the root");
|
|
176
|
+
}
|
|
177
|
+
const stat = lstatSync(absolute, { throwIfNoEntry: false });
|
|
178
|
+
if (!stat) return null;
|
|
179
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
180
|
+
return NONREGULAR_AGENTS_TARGET;
|
|
181
|
+
}
|
|
182
|
+
return readFileSync(absolute, "utf8");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function applyAgentsCheckpoint(
|
|
186
|
+
root: string,
|
|
187
|
+
value: unknown,
|
|
188
|
+
signal?: AbortSignal,
|
|
189
|
+
): Promise<AgentsCheckpointResult> {
|
|
190
|
+
const validation = validateAgentsCheckpointRequest(value);
|
|
191
|
+
if (!validation.ok) {
|
|
192
|
+
return failure({ kind: "artifact", code: "invalid-checkpoint-contract" });
|
|
193
|
+
}
|
|
194
|
+
const request = validation.value;
|
|
195
|
+
if (signal?.aborted) {
|
|
196
|
+
return cancellationFailure();
|
|
197
|
+
}
|
|
198
|
+
const before = regularText(root, request.agentsTarget);
|
|
199
|
+
if (before === NONREGULAR_AGENTS_TARGET) {
|
|
200
|
+
return failure({ kind: "artifact", code: "nonregular-mode" });
|
|
201
|
+
}
|
|
202
|
+
if (!isCurrent(root, request.snapshot as Bound)) {
|
|
203
|
+
return failure({ kind: "stale", code: "stale-snapshot" });
|
|
204
|
+
}
|
|
205
|
+
let targets: string[];
|
|
206
|
+
try {
|
|
207
|
+
targets = diffWritePaths(request.diff).paths;
|
|
208
|
+
} catch {
|
|
209
|
+
return failure({ kind: "artifact", code: "invalid-diff" });
|
|
210
|
+
}
|
|
211
|
+
if (targets.length !== 1 || targets[0] !== request.agentsTarget) {
|
|
212
|
+
return failure({ kind: "artifact", code: "agents-target-mismatch" });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let temp: string | undefined;
|
|
216
|
+
try {
|
|
217
|
+
if (
|
|
218
|
+
(request.agentsImpact === "create-index" && before !== null) ||
|
|
219
|
+
(request.agentsImpact !== "create-index" && before === null)
|
|
220
|
+
) {
|
|
221
|
+
return failure({ kind: "artifact", code: "agents-impact-mismatch" });
|
|
222
|
+
}
|
|
223
|
+
temp = mkdtempSync(path.join(tmpdir(), "cadence-agents-checkpoint-"));
|
|
224
|
+
if (before !== null) {
|
|
225
|
+
const temporaryTarget = path.join(temp, request.agentsTarget);
|
|
226
|
+
mkdirSync(path.dirname(temporaryTarget), { recursive: true });
|
|
227
|
+
copyFileSync(path.join(root, request.agentsTarget), temporaryTarget);
|
|
228
|
+
}
|
|
229
|
+
const bytes = Buffer.from(request.diff, "utf8");
|
|
230
|
+
const candidateCheck = await git(
|
|
231
|
+
temp,
|
|
232
|
+
["apply", "--check", "--recount", "--whitespace=nowarn", "-"],
|
|
233
|
+
bytes,
|
|
234
|
+
signal,
|
|
235
|
+
);
|
|
236
|
+
if (candidateCheck.cancelled || signal?.aborted) {
|
|
237
|
+
return cancellationFailure();
|
|
238
|
+
}
|
|
239
|
+
if (candidateCheck.code !== 0) {
|
|
240
|
+
return failure({ kind: "artifact", code: "git-apply-check-failed" });
|
|
241
|
+
}
|
|
242
|
+
const candidateApply = await git(
|
|
243
|
+
temp,
|
|
244
|
+
["apply", "--recount", "--whitespace=nowarn", "-"],
|
|
245
|
+
bytes,
|
|
246
|
+
signal,
|
|
247
|
+
);
|
|
248
|
+
if (candidateApply.cancelled || signal?.aborted) {
|
|
249
|
+
return cancellationFailure();
|
|
250
|
+
}
|
|
251
|
+
if (candidateApply.code !== 0) {
|
|
252
|
+
return failure({ kind: "artifact", code: "git-apply-failed" });
|
|
253
|
+
}
|
|
254
|
+
const after = regularText(temp, request.agentsTarget);
|
|
255
|
+
if (after === NONREGULAR_AGENTS_TARGET) {
|
|
256
|
+
return failure({ kind: "artifact", code: "nonregular-mode" });
|
|
257
|
+
}
|
|
258
|
+
if (!checkpointContentIsScoped(request.agentsImpact, before, after)) {
|
|
259
|
+
return failure({ kind: "artifact", code: "outside-managed-region" });
|
|
260
|
+
}
|
|
261
|
+
if (!isCurrent(root, request.snapshot as Bound)) {
|
|
262
|
+
return failure({ kind: "stale", code: "stale-snapshot" });
|
|
263
|
+
}
|
|
264
|
+
const check = await git(
|
|
265
|
+
root,
|
|
266
|
+
["apply", "--check", "--recount", "--whitespace=nowarn", "-"],
|
|
267
|
+
bytes,
|
|
268
|
+
signal,
|
|
269
|
+
);
|
|
270
|
+
if (check.cancelled || signal?.aborted) {
|
|
271
|
+
return cancellationFailure();
|
|
272
|
+
}
|
|
273
|
+
if (check.code !== 0) {
|
|
274
|
+
return failure({ kind: "stale", code: "git-apply-check-failed" });
|
|
275
|
+
}
|
|
276
|
+
const apply = await git(
|
|
277
|
+
root,
|
|
278
|
+
["apply", "--recount", "--whitespace=nowarn", "-"],
|
|
279
|
+
bytes,
|
|
280
|
+
);
|
|
281
|
+
if (apply.code !== 0) {
|
|
282
|
+
return failure({ kind: "environment", code: "git-apply-failed" });
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
ok: true,
|
|
286
|
+
target: request.agentsTarget,
|
|
287
|
+
agentsImpact: request.agentsImpact,
|
|
288
|
+
checkExitCode: 0,
|
|
289
|
+
applyExitCode: 0,
|
|
290
|
+
};
|
|
291
|
+
} finally {
|
|
292
|
+
if (temp) rmSync(temp, { recursive: true, force: true });
|
|
293
|
+
}
|
|
63
294
|
}
|
|
64
295
|
|
|
65
296
|
export async function applyRetainedPatch(input: {
|
|
@@ -67,29 +298,22 @@ export async function applyRetainedPatch(input: {
|
|
|
67
298
|
id: string;
|
|
68
299
|
store: ResultStore;
|
|
69
300
|
signal?: AbortSignal;
|
|
70
|
-
}): Promise<
|
|
71
|
-
| { ok: true; targets: string[]; checkExitCode: 0; applyExitCode: 0 }
|
|
72
|
-
| { ok: false; error: string }
|
|
73
|
-
> {
|
|
301
|
+
}): Promise<ApplyCandidateResult> {
|
|
74
302
|
if (input.signal?.aborted) return cancellationFailure();
|
|
75
|
-
const retained = input.store.
|
|
76
|
-
if (!retained) return { ok: false, error: "retained result not found" };
|
|
303
|
+
const retained = input.store.resolveForApply(input.id);
|
|
77
304
|
if (retained.root !== input.root)
|
|
78
|
-
|
|
305
|
+
throw new Error("retained result root mismatch");
|
|
79
306
|
if (!retained.verification && !isCurrent(input.root, retained.snapshot))
|
|
80
|
-
return {
|
|
307
|
+
return failure({ kind: "stale", code: "stale-snapshot" });
|
|
81
308
|
|
|
82
309
|
let targets: string[];
|
|
83
310
|
try {
|
|
84
311
|
targets = diffWritePaths(retained.diff.toString("utf8")).paths;
|
|
85
|
-
} catch
|
|
86
|
-
return {
|
|
312
|
+
} catch {
|
|
313
|
+
return failure({ kind: "artifact", code: "invalid-diff" });
|
|
87
314
|
}
|
|
88
315
|
if (targets.some((path) => !retained.writeSet.includes(path))) {
|
|
89
|
-
return {
|
|
90
|
-
ok: false,
|
|
91
|
-
error: "patch target is outside the declared write set",
|
|
92
|
-
};
|
|
316
|
+
return failure({ kind: "artifact", code: "write-set-mismatch" });
|
|
93
317
|
}
|
|
94
318
|
|
|
95
319
|
if (retained.verification) {
|
|
@@ -98,15 +322,13 @@ export async function applyRetainedPatch(input: {
|
|
|
98
322
|
!retained.lockfile ||
|
|
99
323
|
!retained.dependencyTarget
|
|
100
324
|
) {
|
|
101
|
-
|
|
102
|
-
ok: false,
|
|
103
|
-
error: "retained candidate preflight inputs are incomplete",
|
|
104
|
-
};
|
|
325
|
+
throw new Error("retained candidate preflight inputs are incomplete");
|
|
105
326
|
}
|
|
106
327
|
const preflight = await preflightCandidate({
|
|
107
328
|
root: input.root,
|
|
108
329
|
diff: retained.diff,
|
|
109
330
|
writeSet: retained.writeSet,
|
|
331
|
+
approvedDependencies: retained.approvedDependencies,
|
|
110
332
|
snapshot: retained.snapshot,
|
|
111
333
|
baseline: retained.baseline,
|
|
112
334
|
verification: retained.verification,
|
|
@@ -116,13 +338,25 @@ export async function applyRetainedPatch(input: {
|
|
|
116
338
|
signal: input.signal,
|
|
117
339
|
});
|
|
118
340
|
if (!preflight.ok) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
341
|
+
switch (preflight.kind) {
|
|
342
|
+
case "artifact":
|
|
343
|
+
return failure({
|
|
344
|
+
kind: preflight.kind,
|
|
345
|
+
code: preflight.code,
|
|
346
|
+
...(preflight.excerpt ? { evidence: [preflight.excerpt] } : {}),
|
|
347
|
+
});
|
|
348
|
+
case "stale":
|
|
349
|
+
return failure({ kind: preflight.kind, code: preflight.code });
|
|
350
|
+
case "environment":
|
|
351
|
+
return failure({ kind: preflight.kind, code: preflight.code });
|
|
352
|
+
case "approval-boundary":
|
|
353
|
+
return failure({ kind: preflight.kind, code: preflight.code });
|
|
354
|
+
case "cancelled":
|
|
355
|
+
return failure({ kind: preflight.kind, code: preflight.code });
|
|
356
|
+
}
|
|
123
357
|
}
|
|
124
358
|
if (!isCurrent(input.root, retained.snapshot)) {
|
|
125
|
-
return {
|
|
359
|
+
return failure({ kind: "stale", code: "stale-snapshot" });
|
|
126
360
|
}
|
|
127
361
|
}
|
|
128
362
|
if (input.signal?.aborted) return cancellationFailure();
|
|
@@ -135,10 +369,7 @@ export async function applyRetainedPatch(input: {
|
|
|
135
369
|
);
|
|
136
370
|
if (numstat.cancelled || input.signal?.aborted) return cancellationFailure();
|
|
137
371
|
if (numstat.code !== 0 || numstat.stdout.includes(Buffer.from("-\t-\t"))) {
|
|
138
|
-
return {
|
|
139
|
-
ok: false,
|
|
140
|
-
error: `git numstat screening failed: ${numstat.stderr.toString("utf8")}`,
|
|
141
|
-
};
|
|
372
|
+
return failure({ kind: "artifact", code: "invalid-diff" });
|
|
142
373
|
}
|
|
143
374
|
const summary = await git(
|
|
144
375
|
input.root,
|
|
@@ -153,7 +384,7 @@ export async function applyRetainedPatch(input: {
|
|
|
153
384
|
summary.stdout.toString("utf8"),
|
|
154
385
|
)
|
|
155
386
|
) {
|
|
156
|
-
return {
|
|
387
|
+
return failure({ kind: "artifact", code: "invalid-diff" });
|
|
157
388
|
}
|
|
158
389
|
const check = await git(
|
|
159
390
|
input.root,
|
|
@@ -163,10 +394,7 @@ export async function applyRetainedPatch(input: {
|
|
|
163
394
|
);
|
|
164
395
|
if (check.cancelled || input.signal?.aborted) return cancellationFailure();
|
|
165
396
|
if (check.code !== 0)
|
|
166
|
-
return {
|
|
167
|
-
ok: false,
|
|
168
|
-
error: `git apply --check failed: ${check.stderr.toString("utf8")}`,
|
|
169
|
-
};
|
|
397
|
+
return failure({ kind: "artifact", code: "git-apply-check-failed" });
|
|
170
398
|
if (input.signal?.aborted) return cancellationFailure();
|
|
171
399
|
const apply = await git(
|
|
172
400
|
input.root,
|
|
@@ -174,10 +402,10 @@ export async function applyRetainedPatch(input: {
|
|
|
174
402
|
retained.diff,
|
|
175
403
|
);
|
|
176
404
|
if (apply.code !== 0)
|
|
177
|
-
return {
|
|
178
|
-
ok: false,
|
|
179
|
-
error: `git apply failed: ${apply.stderr.toString("utf8")}`,
|
|
180
|
-
};
|
|
405
|
+
return failure({ kind: "environment", code: "git-apply-failed" });
|
|
181
406
|
input.store.discard(input.id);
|
|
182
|
-
return {
|
|
407
|
+
return {
|
|
408
|
+
ok: true,
|
|
409
|
+
result: { targets, checkExitCode: 0, applyExitCode: 0 },
|
|
410
|
+
};
|
|
183
411
|
}
|