@abelxiaoxing/cadence 1.8.2 → 1.8.4
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/README.md +71 -531
- package/agents/design-explorer.md +15 -23
- package/agents/diagnosis-worker.md +12 -23
- package/agents/implementation-worker.md +16 -34
- package/config/.env.example +2 -5
- package/package.json +1 -1
- package/prompts/abel-diagnose.md +5 -5
- package/src/candidate-context.ts +289 -0
- package/src/contracts.ts +12 -22
- package/src/durable-contracts.ts +1 -5
- package/src/durable-workflow.ts +3 -159
- package/src/evidence-draft.ts +1 -1
- package/src/index.ts +12 -16
- package/src/package-candidate-process.ts +206 -0
- package/src/package-candidate.ts +11 -214
- package/src/package-workflow.ts +5 -42
- package/src/packet-runtime.ts +144 -255
- package/src/phase-execution.ts +134 -230
- package/src/startup-card.ts +17 -23
- package/src/subagent-process.ts +476 -0
- package/src/transport-budget.ts +1 -3
- package/src/workflow-policy.ts +9 -18
- package/src/workflow-state-machine.ts +1 -15
- package/config/routes.example.json +0 -39
- package/src/child-budget.ts +0 -58
- package/src/child-model.ts +0 -142
- package/src/child-session.ts +0 -640
- package/src/parent-provider.ts +0 -375
- package/src/route-policy.ts +0 -545
- package/src/submit-tool.ts +0 -950
- package/src/worker-broker.ts +0 -806
package/src/durable-workflow.ts
CHANGED
|
@@ -22,24 +22,18 @@ interface DurableRunResources extends DurableExecutionResources {
|
|
|
22
22
|
|
|
23
23
|
import { lstatSync, mkdirSync, readFileSync } from "node:fs";
|
|
24
24
|
import path from "node:path";
|
|
25
|
-
import { DatabaseSync } from "node:sqlite";
|
|
26
25
|
import { ApplyTransaction } from "./apply-transaction.ts";
|
|
27
26
|
import { ArtifactStore } from "./artifact-store.ts";
|
|
28
27
|
import { compareCanonicalStrings } from "./canonical.ts";
|
|
29
28
|
import type { ImplementPlan, PlanTaskDraft } from "./delivery-compiler.ts";
|
|
30
|
-
import type { RoutePolicy } from "./route-policy.ts";
|
|
31
29
|
import { observeSafePath } from "./safe-path.ts";
|
|
32
|
-
import { configureSqlite, ensureSqliteSchema } from "./sqlite-schema.ts";
|
|
33
|
-
import { ROUTE_HEALTH_SCHEMA } from "./storage-schema.ts";
|
|
34
30
|
import { TaskLedger } from "./task-ledger.ts";
|
|
35
|
-
import { type RouteHealthStore, RunWorkerBroker } from "./worker-broker.ts";
|
|
36
31
|
import {
|
|
37
32
|
deliveryBoundPaths,
|
|
38
33
|
deliveryTrackingPath,
|
|
39
34
|
hash,
|
|
40
35
|
hasVerificationLifecycle,
|
|
41
36
|
IDENTIFIER,
|
|
42
|
-
implementationRouteRequirements,
|
|
43
37
|
isRecord,
|
|
44
38
|
SHA256,
|
|
45
39
|
type WorkflowApplication,
|
|
@@ -52,125 +46,6 @@ import {
|
|
|
52
46
|
import { WorkflowEngine } from "./workflow-state-machine.ts";
|
|
53
47
|
import { type WorkspaceRevision, WorkspaceStore } from "./workspace-store.ts";
|
|
54
48
|
|
|
55
|
-
type DurableRouteHealth = Parameters<RouteHealthStore["set"]>[1];
|
|
56
|
-
|
|
57
|
-
class DurableRouteHealthStore implements RouteHealthStore {
|
|
58
|
-
readonly #databasePath: string;
|
|
59
|
-
#database?: DatabaseSync;
|
|
60
|
-
#closed = false;
|
|
61
|
-
|
|
62
|
-
constructor(databasePath: string) {
|
|
63
|
-
this.#databasePath = databasePath;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
#open(): DatabaseSync {
|
|
67
|
-
if (this.#closed) throw new Error("route-health-store-closed");
|
|
68
|
-
if (this.#database) return this.#database;
|
|
69
|
-
const database = new DatabaseSync(this.#databasePath);
|
|
70
|
-
try {
|
|
71
|
-
configureSqlite(database);
|
|
72
|
-
ensureSqliteSchema(database, ROUTE_HEALTH_SCHEMA);
|
|
73
|
-
} catch (error) {
|
|
74
|
-
database.close();
|
|
75
|
-
throw error;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
this.#database = database;
|
|
79
|
-
return database;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
get(fingerprint: string): DurableRouteHealth | undefined {
|
|
83
|
-
if (!SHA256.test(fingerprint)) return undefined;
|
|
84
|
-
const row = this.#open()
|
|
85
|
-
.prepare(
|
|
86
|
-
`SELECT state, next_half_open_at, projection_json
|
|
87
|
-
FROM route_health WHERE route_fingerprint = ?`,
|
|
88
|
-
)
|
|
89
|
-
.get(fingerprint) as
|
|
90
|
-
| {
|
|
91
|
-
state: string;
|
|
92
|
-
next_half_open_at: number | null;
|
|
93
|
-
projection_json: string;
|
|
94
|
-
}
|
|
95
|
-
| undefined;
|
|
96
|
-
if (!row || !["healthy", "open", "half-open"].includes(row.state)) {
|
|
97
|
-
return undefined;
|
|
98
|
-
}
|
|
99
|
-
let projection: unknown;
|
|
100
|
-
try {
|
|
101
|
-
projection = JSON.parse(row.projection_json);
|
|
102
|
-
} catch {
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
|
-
if (!isRecord(projection) || projection.state !== row.state) {
|
|
106
|
-
return undefined;
|
|
107
|
-
}
|
|
108
|
-
const retryAt = row.next_half_open_at ?? undefined;
|
|
109
|
-
const lastCode =
|
|
110
|
-
typeof projection.lastCode === "string" &&
|
|
111
|
-
projection.lastCode.length > 0 &&
|
|
112
|
-
projection.lastCode.length <= 128
|
|
113
|
-
? projection.lastCode
|
|
114
|
-
: undefined;
|
|
115
|
-
const probeExpiresAt =
|
|
116
|
-
Number.isSafeInteger(projection.probeExpiresAt) &&
|
|
117
|
-
(projection.probeExpiresAt as number) >= 0
|
|
118
|
-
? (projection.probeExpiresAt as number)
|
|
119
|
-
: undefined;
|
|
120
|
-
return {
|
|
121
|
-
state: row.state as DurableRouteHealth["state"],
|
|
122
|
-
...(retryAt === undefined ? {} : { retryAt }),
|
|
123
|
-
...(projection.probeInFlight === true ? { probeInFlight: true } : {}),
|
|
124
|
-
...(probeExpiresAt === undefined ? {} : { probeExpiresAt }),
|
|
125
|
-
...(lastCode === undefined ? {} : { lastCode }),
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
set(fingerprint: string, health: DurableRouteHealth): void {
|
|
130
|
-
if (
|
|
131
|
-
!SHA256.test(fingerprint) ||
|
|
132
|
-
!["healthy", "open", "half-open"].includes(health.state) ||
|
|
133
|
-
(health.retryAt !== undefined &&
|
|
134
|
-
(!Number.isSafeInteger(health.retryAt) || health.retryAt < 0)) ||
|
|
135
|
-
(health.probeExpiresAt !== undefined &&
|
|
136
|
-
(!Number.isSafeInteger(health.probeExpiresAt) ||
|
|
137
|
-
health.probeExpiresAt < 0))
|
|
138
|
-
) {
|
|
139
|
-
throw new Error("route-health-fact-invalid");
|
|
140
|
-
}
|
|
141
|
-
const projection = {
|
|
142
|
-
state: health.state,
|
|
143
|
-
...(health.probeInFlight ? { probeInFlight: true } : {}),
|
|
144
|
-
...(health.probeExpiresAt === undefined
|
|
145
|
-
? {}
|
|
146
|
-
: { probeExpiresAt: health.probeExpiresAt }),
|
|
147
|
-
...(health.lastCode ? { lastCode: health.lastCode.slice(0, 128) } : {}),
|
|
148
|
-
};
|
|
149
|
-
this.#open()
|
|
150
|
-
.prepare(
|
|
151
|
-
`INSERT INTO route_health(
|
|
152
|
-
route_fingerprint, state, next_half_open_at, projection_json
|
|
153
|
-
) VALUES (?, ?, ?, ?)
|
|
154
|
-
ON CONFLICT(route_fingerprint) DO UPDATE SET
|
|
155
|
-
state = excluded.state,
|
|
156
|
-
next_half_open_at = excluded.next_half_open_at,
|
|
157
|
-
projection_json = excluded.projection_json`,
|
|
158
|
-
)
|
|
159
|
-
.run(
|
|
160
|
-
fingerprint,
|
|
161
|
-
health.state,
|
|
162
|
-
health.retryAt ?? null,
|
|
163
|
-
JSON.stringify(projection),
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
close(): void {
|
|
168
|
-
if (this.#closed) return;
|
|
169
|
-
this.#database?.close();
|
|
170
|
-
this.#closed = true;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
49
|
function ensurePrivateRunDirectory(directory: string): void {
|
|
175
50
|
const existing = lstatSync(directory, { throwIfNoEntry: false });
|
|
176
51
|
if (existing) {
|
|
@@ -196,19 +71,10 @@ class DurableWorkflowComposition
|
|
|
196
71
|
readonly #phase: PhaseExecution;
|
|
197
72
|
readonly #verification: ChangeVerification;
|
|
198
73
|
readonly #options: DurableWorkflowEngineOptions;
|
|
199
|
-
readonly #routeHealth: DurableRouteHealthStore;
|
|
200
|
-
#broker: RunWorkerBroker;
|
|
201
74
|
readonly #runs = new Map<string, DurableRunResources>();
|
|
202
75
|
|
|
203
76
|
constructor(options: DurableWorkflowEngineOptions) {
|
|
204
77
|
this.#options = options;
|
|
205
|
-
this.#routeHealth = new DurableRouteHealthStore(
|
|
206
|
-
options.stateRoot.databasePath,
|
|
207
|
-
);
|
|
208
|
-
this.#broker = new RunWorkerBroker(options.routePolicy, {
|
|
209
|
-
...(options.now ? { now: options.now } : {}),
|
|
210
|
-
healthStore: this.#routeHealth,
|
|
211
|
-
});
|
|
212
78
|
this.#verification = new ChangeVerification(options, {
|
|
213
79
|
prepareResources: (input, signal) =>
|
|
214
80
|
this.#prepareResources(input, signal),
|
|
@@ -217,7 +83,7 @@ class DurableWorkflowComposition
|
|
|
217
83
|
revalidatePhasePolicy: (...args) =>
|
|
218
84
|
this.#phase.revalidatePhasePolicy(...args),
|
|
219
85
|
});
|
|
220
|
-
this.#phase = new PhaseExecution(options,
|
|
86
|
+
this.#phase = new PhaseExecution(options, {
|
|
221
87
|
prepareResources: (input, signal) =>
|
|
222
88
|
this.#prepareResources(input, signal),
|
|
223
89
|
ledger: (resources, revision) => this.#ledger(resources, revision),
|
|
@@ -229,14 +95,6 @@ class DurableWorkflowComposition
|
|
|
229
95
|
});
|
|
230
96
|
}
|
|
231
97
|
|
|
232
|
-
updateRoutePolicy(policy: RoutePolicy): void {
|
|
233
|
-
this.#broker.updatePolicy(policy);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
routePolicyStatus(): Record<string, unknown> {
|
|
237
|
-
return this.#broker.status();
|
|
238
|
-
}
|
|
239
|
-
|
|
240
98
|
#runRoot(runId: string): string {
|
|
241
99
|
if (!IDENTIFIER.test(runId)) throw new Error("workflow-run-id-invalid");
|
|
242
100
|
const dataRoot = path.join(this.#options.stateRoot.rootDir, "run-data");
|
|
@@ -770,21 +628,8 @@ class DurableWorkflowComposition
|
|
|
770
628
|
return revision.entries[input.path]?.kind === "file";
|
|
771
629
|
}
|
|
772
630
|
|
|
773
|
-
rebind(
|
|
774
|
-
const
|
|
775
|
-
runId: input.runId,
|
|
776
|
-
taskId: input.taskId,
|
|
777
|
-
role: input.role,
|
|
778
|
-
routeId: input.routeId,
|
|
779
|
-
requirements: implementationRouteRequirements({ task: input.task }),
|
|
780
|
-
});
|
|
781
|
-
return rebound.ok
|
|
782
|
-
? {
|
|
783
|
-
ok: true as const,
|
|
784
|
-
routeId: rebound.route.id,
|
|
785
|
-
routeFingerprint: rebound.route.fingerprint,
|
|
786
|
-
}
|
|
787
|
-
: { ok: false as const, code: rebound.code };
|
|
631
|
+
rebind(_input: Parameters<WorkflowWorker["rebind"]>[0]) {
|
|
632
|
+
return { ok: false as const, code: "route-configuration-removed" };
|
|
788
633
|
}
|
|
789
634
|
|
|
790
635
|
async prepare(
|
|
@@ -912,7 +757,6 @@ class DurableWorkflowComposition
|
|
|
912
757
|
this.#closeResources(resources);
|
|
913
758
|
}
|
|
914
759
|
this.#runs.clear();
|
|
915
|
-
this.#routeHealth.close();
|
|
916
760
|
}
|
|
917
761
|
runAttempt(input: Parameters<WorkflowWorker["runAttempt"]>[0]) {
|
|
918
762
|
return this.#phase.runAttempt(input);
|
package/src/evidence-draft.ts
CHANGED
|
@@ -21,7 +21,7 @@ export function evidenceDraftSchema(requestId: string, role: string) {
|
|
|
21
21
|
id: Type.Optional(
|
|
22
22
|
Type.Literal(requestId, {
|
|
23
23
|
description:
|
|
24
|
-
"Omit: bound by the
|
|
24
|
+
"Omit: bound by the parent result adapter. If supplied, must match this request.",
|
|
25
25
|
}),
|
|
26
26
|
),
|
|
27
27
|
role: Type.Optional(Type.Literal(role)),
|
package/src/index.ts
CHANGED
|
@@ -59,7 +59,7 @@ import { canonicalJson } from "./implement-graph.ts";
|
|
|
59
59
|
import { PACKET_ACTIONS, PacketRuntime } from "./packet-runtime.ts";
|
|
60
60
|
|
|
61
61
|
import { RunStoreFormatError, RunStoreMigrationError } from "./run-store.ts";
|
|
62
|
-
import {
|
|
62
|
+
import { startupCardLines } from "./startup-card.ts";
|
|
63
63
|
import { StateRootError } from "./state-root.ts";
|
|
64
64
|
import {
|
|
65
65
|
ACTIVITY_DETAILS_KEY,
|
|
@@ -1792,23 +1792,19 @@ export function registerWorkflowControl(
|
|
|
1792
1792
|
}
|
|
1793
1793
|
|
|
1794
1794
|
export default function register(pi: ExtensionAPI): void {
|
|
1795
|
+
// Process children are deliberately started without extensions as a second
|
|
1796
|
+
// defense. Keep this guard for hosts that inject the extension explicitly.
|
|
1797
|
+
if (process.env.CADENCE_SUBAGENT_CHILD === "1") return;
|
|
1795
1798
|
registerWorkflowControl(pi);
|
|
1796
|
-
// Presentation only:
|
|
1799
|
+
// Presentation only: use the host's subdued one-shot notification rather
|
|
1800
|
+
// than a persistent widget; never persist a dismissal or inject a model message.
|
|
1797
1801
|
pi.on("session_start", (_event, ctx) => {
|
|
1798
1802
|
if (!ctx.hasUI) return;
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
}),
|
|
1806
|
-
);
|
|
1807
|
-
});
|
|
1808
|
-
pi.on("agent_start", (_event, ctx) => {
|
|
1809
|
-
if (ctx.hasUI) ctx.ui.setWidget(STARTUP_CARD_KEY, undefined);
|
|
1810
|
-
});
|
|
1811
|
-
pi.on("session_shutdown", (_event, ctx) => {
|
|
1812
|
-
if (ctx.hasUI) ctx.ui.setWidget(STARTUP_CARD_KEY, undefined);
|
|
1803
|
+
const lines = startupCardLines({
|
|
1804
|
+
cwd: ctx.cwd,
|
|
1805
|
+
home: homedir(),
|
|
1806
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
1807
|
+
});
|
|
1808
|
+
ctx.ui.notify(lines.at(-1) ?? "Cadence研究配置已加载。", "info");
|
|
1813
1809
|
});
|
|
1814
1810
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import type { VerificationFailureSummary } from "./contracts.ts";
|
|
6
|
+
import { isValidRelativePath } from "./contracts.ts";
|
|
7
|
+
import type { DurableWorkflowEngineOptions } from "./durable-contracts.ts";
|
|
8
|
+
import type { ParentModelSource } from "./model-source.ts";
|
|
9
|
+
import { isSafeRegularFile } from "./safe-path.ts";
|
|
10
|
+
import {
|
|
11
|
+
buildSubagentPrompt,
|
|
12
|
+
runSubagentProcess,
|
|
13
|
+
SUBAGENT_LIMITS,
|
|
14
|
+
} from "./subagent-process.ts";
|
|
15
|
+
import type { DurableCandidateProposal } from "./workflow-policy.ts";
|
|
16
|
+
|
|
17
|
+
function permitsContextRead(
|
|
18
|
+
relative: string,
|
|
19
|
+
roots: readonly string[],
|
|
20
|
+
): boolean {
|
|
21
|
+
return (
|
|
22
|
+
isValidRelativePath(relative) &&
|
|
23
|
+
relative.split("/").every((part) => !part.startsWith(".")) &&
|
|
24
|
+
!/\.(?:pem|key|p12|pfx)$/iu.test(relative) &&
|
|
25
|
+
roots.some((root) => relative === root || relative.startsWith(`${root}/`))
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function restoreWorkspace(root: string, baseline: string): void {
|
|
30
|
+
rmSync(root, { recursive: true, force: true });
|
|
31
|
+
mkdirSync(root, { recursive: true });
|
|
32
|
+
cpSync(baseline, root, { recursive: true, force: true, dereference: false });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Produce a Git patch from two disposable trees without invoking a shell. */
|
|
36
|
+
function workspaceDiff(baseline: string, proposal: string): Uint8Array {
|
|
37
|
+
const parent = path.dirname(baseline);
|
|
38
|
+
const proposalSnapshot = mkdtempSync(
|
|
39
|
+
path.join(parent, "cadence-subagent-proposal-"),
|
|
40
|
+
);
|
|
41
|
+
cpSync(proposal, proposalSnapshot, {
|
|
42
|
+
recursive: true,
|
|
43
|
+
force: true,
|
|
44
|
+
dereference: false,
|
|
45
|
+
});
|
|
46
|
+
try {
|
|
47
|
+
const baseName = path.basename(baseline);
|
|
48
|
+
const proposalName = path.basename(proposalSnapshot);
|
|
49
|
+
try {
|
|
50
|
+
execFileSync(
|
|
51
|
+
"git",
|
|
52
|
+
[
|
|
53
|
+
"-c",
|
|
54
|
+
"core.autocrlf=false",
|
|
55
|
+
"-c",
|
|
56
|
+
"core.eol=lf",
|
|
57
|
+
"diff",
|
|
58
|
+
"--no-index",
|
|
59
|
+
"--binary",
|
|
60
|
+
"--no-color",
|
|
61
|
+
"--",
|
|
62
|
+
baseName,
|
|
63
|
+
proposalName,
|
|
64
|
+
],
|
|
65
|
+
{
|
|
66
|
+
cwd: parent,
|
|
67
|
+
encoding: "buffer",
|
|
68
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
69
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
return Buffer.alloc(0);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (!error || typeof error !== "object") return Buffer.alloc(0);
|
|
75
|
+
const output = (error as { stdout?: Buffer }).stdout;
|
|
76
|
+
if (!output) return Buffer.alloc(0);
|
|
77
|
+
const pathPrefixes = [
|
|
78
|
+
`a/${baseName}/`,
|
|
79
|
+
`a/${proposalName}/`,
|
|
80
|
+
`b/${baseName}/`,
|
|
81
|
+
`b/${proposalName}/`,
|
|
82
|
+
];
|
|
83
|
+
const normalizeHeader = (line: string): string => {
|
|
84
|
+
let normalized = line;
|
|
85
|
+
for (const prefix of pathPrefixes) {
|
|
86
|
+
const replacement = prefix.startsWith("a/") ? "a/" : "b/";
|
|
87
|
+
normalized = normalized.replaceAll(prefix, replacement);
|
|
88
|
+
}
|
|
89
|
+
return normalized;
|
|
90
|
+
};
|
|
91
|
+
const normalized = output
|
|
92
|
+
.toString("utf8")
|
|
93
|
+
.split("\n")
|
|
94
|
+
.map((line) =>
|
|
95
|
+
line.startsWith("diff --git ") ||
|
|
96
|
+
line.startsWith("--- ") ||
|
|
97
|
+
line.startsWith("+++ ")
|
|
98
|
+
? normalizeHeader(line)
|
|
99
|
+
: line,
|
|
100
|
+
)
|
|
101
|
+
.join("\n");
|
|
102
|
+
return Buffer.from(normalized, "utf8");
|
|
103
|
+
}
|
|
104
|
+
} finally {
|
|
105
|
+
rmSync(proposalSnapshot, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function proposeProcessPackageCandidate(
|
|
110
|
+
input: Parameters<DurableWorkflowEngineOptions["proposeCandidate"]>[0],
|
|
111
|
+
context: ParentModelSource | undefined,
|
|
112
|
+
implementationAgent: { content: string },
|
|
113
|
+
verificationDiagnostics: VerificationFailureSummary[] = [],
|
|
114
|
+
): Promise<DurableCandidateProposal> {
|
|
115
|
+
if (!context) return { kind: "paused", code: "parent-context-unavailable" };
|
|
116
|
+
if (input.signal.aborted)
|
|
117
|
+
return { kind: "operation-cancelled", code: "cancelled" };
|
|
118
|
+
const phase = input.task.phases[input.phase];
|
|
119
|
+
if (!phase) return { kind: "paused", code: "task-phase-unavailable" };
|
|
120
|
+
const taskPhases = Object.values(input.task.phases);
|
|
121
|
+
const read = [
|
|
122
|
+
...new Set(
|
|
123
|
+
taskPhases.flatMap((boundary) => [
|
|
124
|
+
...boundary.read,
|
|
125
|
+
...boundary.write,
|
|
126
|
+
...boundary.delete,
|
|
127
|
+
]),
|
|
128
|
+
),
|
|
129
|
+
].sort();
|
|
130
|
+
const write = input.artifactCorrection
|
|
131
|
+
? [...new Set(taskPhases.flatMap((boundary) => boundary.write))].sort()
|
|
132
|
+
: [...phase.write];
|
|
133
|
+
const deletePaths = input.artifactCorrection
|
|
134
|
+
? [...new Set(taskPhases.flatMap((boundary) => boundary.delete))].sort()
|
|
135
|
+
: [...phase.delete];
|
|
136
|
+
const allowedContext = (input.contextReadPaths ?? []).filter(
|
|
137
|
+
(relative) =>
|
|
138
|
+
permitsContextRead(relative, input.task.roots) &&
|
|
139
|
+
isSafeRegularFile(input.workspaceRoot, relative),
|
|
140
|
+
);
|
|
141
|
+
const allowedPaths = [...new Set([...read, ...allowedContext])].sort();
|
|
142
|
+
const baseline = mkdtempSync(
|
|
143
|
+
path.join(os.tmpdir(), "cadence-subagent-baseline-"),
|
|
144
|
+
);
|
|
145
|
+
try {
|
|
146
|
+
cpSync(input.workspaceRoot, baseline, {
|
|
147
|
+
recursive: true,
|
|
148
|
+
force: true,
|
|
149
|
+
dereference: false,
|
|
150
|
+
});
|
|
151
|
+
const prompt = buildSubagentPrompt({
|
|
152
|
+
role: "implementation-worker",
|
|
153
|
+
agentContent: implementationAgent.content,
|
|
154
|
+
objective: input.task.objective,
|
|
155
|
+
context: [
|
|
156
|
+
input.task.context.agents,
|
|
157
|
+
input.task.context.contract,
|
|
158
|
+
JSON.stringify({
|
|
159
|
+
taskId: input.taskId,
|
|
160
|
+
phase: input.phase,
|
|
161
|
+
readSet: allowedPaths,
|
|
162
|
+
writeSet: write,
|
|
163
|
+
deleteSet: deletePaths,
|
|
164
|
+
verificationDiagnostics,
|
|
165
|
+
recoveryFeedback: input.recoveryFeedback,
|
|
166
|
+
}),
|
|
167
|
+
].join("\n\n"),
|
|
168
|
+
finalConvention:
|
|
169
|
+
"Edit the disposable workspace directly. Finish with a plain-text summary; the parent will derive and validate the candidate diff.",
|
|
170
|
+
});
|
|
171
|
+
const child = await runSubagentProcess({
|
|
172
|
+
role: "implementation-worker",
|
|
173
|
+
cwd: input.workspaceRoot,
|
|
174
|
+
prompt,
|
|
175
|
+
model: context.model
|
|
176
|
+
? { provider: context.model.provider, id: context.model.id }
|
|
177
|
+
: undefined,
|
|
178
|
+
signal: input.signal,
|
|
179
|
+
timeoutMs: SUBAGENT_LIMITS.timeoutMs,
|
|
180
|
+
onEvent: (event) => {
|
|
181
|
+
if (event.type === "agent_start") input.onRequestStart?.();
|
|
182
|
+
if (event.type === "tool_execution_start") input.onProgress?.();
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
if (child.status === "cancelled")
|
|
186
|
+
return { kind: "operation-cancelled", code: "cancelled" };
|
|
187
|
+
if (child.status === "timed-out")
|
|
188
|
+
return { kind: "paused", code: "child-timeout" };
|
|
189
|
+
if (child.status === "output-limit")
|
|
190
|
+
return { kind: "retryable", code: "needs-task-split" };
|
|
191
|
+
if (child.status !== "completed")
|
|
192
|
+
return { kind: "retryable", code: "transport-failure" };
|
|
193
|
+
const bytes = workspaceDiff(baseline, input.workspaceRoot);
|
|
194
|
+
// Existing parent code applies the candidate patch to this root. Restore the
|
|
195
|
+
// baseline first so child edits cannot be applied twice.
|
|
196
|
+
restoreWorkspace(input.workspaceRoot, baseline);
|
|
197
|
+
if (bytes.length === 0)
|
|
198
|
+
return { kind: "retryable", code: "candidate-diff-invalid" };
|
|
199
|
+
return { kind: "candidate", bytes };
|
|
200
|
+
} finally {
|
|
201
|
+
// A child is never allowed to leave edits in the proposal root: the
|
|
202
|
+
// parent-side apply/verification path consumes the generated bytes.
|
|
203
|
+
if (existsSync(baseline)) restoreWorkspace(input.workspaceRoot, baseline);
|
|
204
|
+
rmSync(baseline, { recursive: true, force: true });
|
|
205
|
+
}
|
|
206
|
+
}
|