@abelxiaoxing/cadence 1.3.0 → 1.4.1

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.
Files changed (76) hide show
  1. package/README.md +172 -13
  2. package/agents/design-explorer.md +17 -11
  3. package/agents/diagnosis-worker.md +9 -1
  4. package/agents/implementation-worker.md +3 -1
  5. package/config/plan-draft.example.json +122 -0
  6. package/config/plan-draft.multiple-tasks.example.json +345 -0
  7. package/package.json +9 -5
  8. package/prompts/abel-design.md +141 -38
  9. package/prompts/abel-diagnose.md +10 -1
  10. package/prompts/abel-implement.md +118 -20
  11. package/prompts/abel-init.md +9 -2
  12. package/src/activity-contracts.ts +82 -0
  13. package/src/apply-transaction.ts +16 -14
  14. package/src/artifact-store.ts +17 -0
  15. package/src/candidate-artifact.ts +143 -0
  16. package/src/candidate-patch.ts +33 -12
  17. package/src/candidate-workspace.ts +74 -0
  18. package/src/canonical.ts +16 -0
  19. package/src/change-contract-schema.ts +191 -0
  20. package/src/change-contract.ts +273 -0
  21. package/src/change-verification.ts +896 -0
  22. package/src/child-model.ts +129 -0
  23. package/src/child-session.ts +296 -328
  24. package/src/contracts.ts +325 -109
  25. package/src/control-contracts.ts +51 -1
  26. package/src/delivery-compiler.ts +241 -141
  27. package/src/delivery-revision.ts +42 -0
  28. package/src/design-control.ts +182 -15
  29. package/src/design-diagnostics.ts +150 -0
  30. package/src/design-journal.ts +216 -72
  31. package/src/durable-contracts.ts +210 -0
  32. package/src/durable-workflow.ts +963 -0
  33. package/src/evidence-draft.ts +122 -0
  34. package/src/implement-graph.ts +14 -16
  35. package/src/implement-plan.ts +70 -0
  36. package/src/index.ts +341 -1405
  37. package/src/isolation-backend.ts +84 -23
  38. package/src/model-source.ts +22 -0
  39. package/src/package-candidate.ts +224 -0
  40. package/src/package-delivery.ts +486 -0
  41. package/src/package-verification.ts +509 -0
  42. package/src/package-workflow.ts +253 -0
  43. package/src/packet-runtime.ts +27 -48
  44. package/src/parent-provider.ts +86 -198
  45. package/src/phase-execution.ts +2248 -0
  46. package/src/pi-adapter.ts +19 -0
  47. package/src/plan-draft-summary.ts +146 -0
  48. package/src/plan-draft.ts +493 -0
  49. package/src/route-policy.ts +1 -11
  50. package/src/run-state.ts +4 -16
  51. package/src/run-store.ts +5 -144
  52. package/src/scoped-grep-worker.mjs +23 -0
  53. package/src/scoped-tools.ts +312 -116
  54. package/src/sqlite-schema.ts +171 -0
  55. package/src/storage-schema.ts +352 -0
  56. package/src/subagent-activity.ts +46 -67
  57. package/src/submit-tool.ts +40 -61
  58. package/src/task-ledger.ts +14 -55
  59. package/src/transport-budget.ts +32 -0
  60. package/src/verification-capability.ts +298 -102
  61. package/src/verification-environment.ts +213 -0
  62. package/src/verification-identity.ts +77 -0
  63. package/src/worker-broker.ts +134 -91
  64. package/src/workflow-engine.ts +28 -8285
  65. package/src/workflow-policy.ts +1388 -0
  66. package/src/workflow-recovery-policy.ts +95 -0
  67. package/src/workflow-scheduling.ts +105 -0
  68. package/src/workflow-state-machine.ts +4086 -0
  69. package/src/workflow-status.ts +384 -0
  70. package/src/workspace-io-entry.mjs +58 -0
  71. package/src/workspace-io-worker.mjs +991 -0
  72. package/src/workspace-io.ts +84 -0
  73. package/src/workspace-store.ts +125 -19
  74. package/skills/abel-workflow/SKILL.md +0 -205
  75. package/src/empty-resource-loader.ts +0 -38
  76. package/src/parent-payload-bridge.ts +0 -523
@@ -15,6 +15,7 @@ export interface IsolationRunInput {
15
15
  mounts?: IsolationMount[];
16
16
  environment?: Record<string, string>;
17
17
  signal?: AbortSignal;
18
+ outputWitness?: string;
18
19
  }
19
20
 
20
21
  export type IsolationRunResult =
@@ -24,6 +25,11 @@ export type IsolationRunResult =
24
25
  exitCode: number;
25
26
  stdout: string;
26
27
  stderr: string;
28
+ outputWitnessMatched?: boolean;
29
+ logs: {
30
+ stdout: { bytes: number; truncated: boolean };
31
+ stderr: { bytes: number; truncated: boolean };
32
+ };
27
33
  }
28
34
  | {
29
35
  ok: false;
@@ -31,14 +37,13 @@ export type IsolationRunResult =
31
37
  code:
32
38
  | "isolation-backend-unavailable"
33
39
  | "isolation-backend-launch-failed"
34
- | "isolation-execution-timeout"
35
- | "isolation-output-limit-exceeded";
40
+ | "isolation-execution-timeout";
36
41
  }
37
42
  | { ok: false; state: "cancelled"; code: "cancelled" };
38
43
 
39
44
  export const ISOLATION_RUN_LIMITS = Object.freeze({
40
45
  timeoutMs: 10 * 60_000,
41
- maxOutputBytes: 1024 * 1024,
46
+ maxOutputBytes: 256 * 1024,
42
47
  terminateGraceMs: 1_000,
43
48
  });
44
49
 
@@ -64,6 +69,59 @@ function validEnvironmentName(name: string): boolean {
64
69
  return /^[A-Za-z_][A-Za-z0-9_]*$/u.test(name);
65
70
  }
66
71
 
72
+ /** Retains independent head/tail copies; discarded chunks never stay referenced. */
73
+ class BoundedLog {
74
+ bytes = 0;
75
+ #head = Buffer.alloc(0);
76
+ #tail = Buffer.alloc(0);
77
+ readonly limit: number;
78
+ readonly #witness?: Buffer;
79
+ #witnessTail = Buffer.alloc(0);
80
+ witnessMatched = false;
81
+ constructor(limit: number, witness?: string) {
82
+ this.limit = limit;
83
+ if (witness) this.#witness = Buffer.from(witness);
84
+ }
85
+ append(chunk: Buffer | string): void {
86
+ const input = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
87
+ this.bytes += input.byteLength;
88
+ if (this.#witness && !this.witnessMatched) {
89
+ const searchable = Buffer.concat([this.#witnessTail, input]);
90
+ this.witnessMatched = searchable.includes(this.#witness);
91
+ this.#witnessTail = this.witnessMatched
92
+ ? Buffer.alloc(0)
93
+ : Buffer.from(
94
+ searchable.subarray(
95
+ Math.max(0, searchable.length - this.#witness.length + 1),
96
+ ),
97
+ );
98
+ }
99
+ const headLimit = Math.ceil(this.limit / 2);
100
+ const take = Math.min(input.length, headLimit - this.#head.length);
101
+ if (take > 0)
102
+ this.#head = Buffer.concat([this.#head, input.subarray(0, take)]);
103
+ const rest = input.subarray(take);
104
+ const tailLimit = this.limit - headLimit;
105
+ if (rest.length && tailLimit) {
106
+ this.#tail =
107
+ rest.length >= tailLimit
108
+ ? Buffer.from(rest.subarray(-tailLimit))
109
+ : Buffer.concat([
110
+ this.#tail.subarray(
111
+ Math.max(0, this.#tail.length + rest.length - tailLimit),
112
+ ),
113
+ rest,
114
+ ]);
115
+ }
116
+ }
117
+ text(): string {
118
+ return Buffer.concat([this.#head, this.#tail]).toString("utf8");
119
+ }
120
+ metadata() {
121
+ return { bytes: this.bytes, truncated: this.bytes > this.limit };
122
+ }
123
+ }
124
+
67
125
  /** Linux isolation capability. Lack of Bubblewrap is a pause, never a fallback. */
68
126
  export class BubblewrapIsolationBackend {
69
127
  readonly #bwrapPath: string;
@@ -103,6 +161,13 @@ export class BubblewrapIsolationBackend {
103
161
  }
104
162
 
105
163
  async run(input: IsolationRunInput): Promise<IsolationRunResult> {
164
+ if (
165
+ input.outputWitness !== undefined &&
166
+ (typeof input.outputWitness !== "string" ||
167
+ input.outputWitness.length < 1 ||
168
+ input.outputWitness.length > 512)
169
+ )
170
+ throw new Error("isolation-output-witness-invalid");
106
171
  if (!(await this.available())) {
107
172
  return {
108
173
  ok: false,
@@ -165,9 +230,8 @@ export class BubblewrapIsolationBackend {
165
230
 
166
231
  return new Promise((resolve) => {
167
232
  let settled = false;
168
- const stdout: Buffer[] = [];
169
- const stderr: Buffer[] = [];
170
- let capturedBytes = 0;
233
+ const stdout = new BoundedLog(this.#maxOutputBytes, input.outputWitness);
234
+ const stderr = new BoundedLog(this.#maxOutputBytes, input.outputWitness);
171
235
  let child: ReturnType<typeof spawn>;
172
236
  let executionTimer: ReturnType<typeof setTimeout> | undefined;
173
237
  let killTimer: ReturnType<typeof setTimeout> | undefined;
@@ -207,21 +271,11 @@ export class BubblewrapIsolationBackend {
207
271
  });
208
272
  return;
209
273
  }
210
- const capture = (destination: Buffer[], chunk: Buffer | string): void => {
211
- if (settled || terminationResult) return;
212
- const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
213
- const remaining = this.#maxOutputBytes - capturedBytes;
214
- if (remaining > 0) destination.push(bytes.subarray(0, remaining));
215
- if (bytes.byteLength > remaining) {
216
- capturedBytes = this.#maxOutputBytes;
217
- terminate({
218
- ok: false,
219
- state: "paused",
220
- code: "isolation-output-limit-exceeded",
221
- });
222
- return;
223
- }
224
- capturedBytes += bytes.byteLength;
274
+ const capture = (
275
+ destination: BoundedLog,
276
+ chunk: Buffer | string,
277
+ ): void => {
278
+ if (!settled && !terminationResult) destination.append(chunk);
225
279
  };
226
280
  child.stdout?.on("data", (chunk: Buffer | string) => {
227
281
  capture(stdout, chunk);
@@ -244,8 +298,15 @@ export class BubblewrapIsolationBackend {
244
298
  ok: true,
245
299
  state: "completed",
246
300
  exitCode: code ?? 1,
247
- stdout: Buffer.concat(stdout).toString("utf8"),
248
- stderr: Buffer.concat(stderr).toString("utf8"),
301
+ stdout: stdout.text(),
302
+ stderr: stderr.text(),
303
+ logs: { stdout: stdout.metadata(), stderr: stderr.metadata() },
304
+ ...(input.outputWitness
305
+ ? {
306
+ outputWitnessMatched:
307
+ stdout.witnessMatched || stderr.witnessMatched,
308
+ }
309
+ : {}),
249
310
  },
250
311
  );
251
312
  });
@@ -0,0 +1,22 @@
1
+ import type { Model, Provider } from "@earendil-works/pi-ai";
2
+
3
+ /** Code-owned capability boundary. No session events, tools, UI or host context. */
4
+ export interface ParentModelSource {
5
+ readonly model?: Model<string>;
6
+ readonly modelRegistry: {
7
+ getProvider(id: string): Provider | undefined;
8
+ getApiKeyAndHeaders(model: Model<string>): Promise<
9
+ | {
10
+ ok: true;
11
+ apiKey?: string;
12
+ headers?: Record<string, string | null>;
13
+ baseUrl?: string;
14
+ env?: Record<string, string>;
15
+ }
16
+ | { ok: false }
17
+ >;
18
+ };
19
+ }
20
+ export interface PackageContext extends ParentModelSource {
21
+ readonly cwd: string;
22
+ }
@@ -0,0 +1,224 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+ import { runChildSession } from "./child-session.ts";
4
+ import { LIMITS } from "./contracts.ts";
5
+ import type { DurableWorkflowEngineOptions } from "./durable-contracts.ts";
6
+ import type { ParentModelSource } from "./model-source.ts";
7
+ import { runtimeForWorkerRoute } from "./parent-provider.ts";
8
+ import type { WorkerRoutePolicy } from "./route-policy.ts";
9
+ import { isSafeRegularFile } from "./safe-path.ts";
10
+ import {
11
+ classifyCandidateContextRequest,
12
+ permitsContextRead,
13
+ } from "./submit-tool.ts";
14
+ import { transportFailureError } from "./transport-budget.ts";
15
+ import type { DurableCandidateProposal } from "./workflow-policy.ts";
16
+
17
+ function childRequestId(
18
+ operationId: string,
19
+ taskId: string,
20
+ phase: string,
21
+ ): string {
22
+ return `child-${createHash("sha256").update(`${operationId}\0${taskId}\0${phase}`).digest("hex").slice(0, 40)}`;
23
+ }
24
+ export async function proposePackageCandidate(
25
+ input: Parameters<DurableWorkflowEngineOptions["proposeCandidate"]>[0],
26
+ context: ParentModelSource | undefined,
27
+ implementationAgent: { content: string },
28
+ ): Promise<DurableCandidateProposal> {
29
+ if (!context) {
30
+ return { kind: "paused", code: "parent-context-unavailable" };
31
+ }
32
+ const phaseRuntime = await runtimeForWorkerRoute(
33
+ input.route as WorkerRoutePolicy,
34
+ context,
35
+ input.signal,
36
+ process.env,
37
+ );
38
+ if (!phaseRuntime.ok) {
39
+ if (phaseRuntime.failure.kind === "cancelled") {
40
+ return { kind: "operation-cancelled", code: "cancelled" };
41
+ }
42
+ throw new Error(phaseRuntime.failure.code);
43
+ }
44
+ const phase = input.task.phases[input.phase];
45
+ if (!phase) {
46
+ return { kind: "paused", code: "task-phase-unavailable" };
47
+ }
48
+ const taskPhases = Object.values(input.task.phases);
49
+ const executionBoundary = input.artifactCorrection
50
+ ? {
51
+ read: [
52
+ ...new Set(
53
+ taskPhases.flatMap((boundary) => [
54
+ ...boundary.read,
55
+ ...boundary.write,
56
+ ...boundary.delete,
57
+ ]),
58
+ ),
59
+ ].sort(),
60
+ write: [
61
+ ...new Set(taskPhases.flatMap((boundary) => boundary.write)),
62
+ ].sort(),
63
+ delete: [
64
+ ...new Set(taskPhases.flatMap((boundary) => boundary.delete)),
65
+ ].sort(),
66
+ }
67
+ : {
68
+ read: [
69
+ ...new Set(
70
+ taskPhases.flatMap((boundary) => [
71
+ ...boundary.read,
72
+ ...boundary.write,
73
+ ...boundary.delete,
74
+ ]),
75
+ ),
76
+ ].sort(),
77
+ write: [...phase.write],
78
+ delete: [...phase.delete],
79
+ };
80
+ executionBoundary.read = [
81
+ ...new Set([
82
+ ...executionBoundary.read,
83
+ ...(input.contextReadPaths ?? []).filter(
84
+ (relative) =>
85
+ permitsContextRead(relative, input.task.roots) &&
86
+ isSafeRegularFile(input.workspaceRoot, relative),
87
+ ),
88
+ ]),
89
+ ].sort();
90
+ const phaseContract = {
91
+ candidateId: input.candidateArtifact.identity.candidateId,
92
+ taskId: input.taskId,
93
+ phase: input.phase,
94
+ readSet: executionBoundary.read,
95
+ writeSet: executionBoundary.write,
96
+ deleteSet: executionBoundary.delete,
97
+ verification: structuredClone(phase.verification),
98
+ agentsImpact: input.task.agents.impact,
99
+ agentsTarget: input.task.agents.target ?? null,
100
+ agentsManagedOnly: true,
101
+ agentsWriteAllowed: false,
102
+ impactClosure: structuredClone(input.task.impactClosure),
103
+ ...(input.artifactCorrection
104
+ ? { artifactCorrection: structuredClone(input.artifactCorrection) }
105
+ : {}),
106
+ ...(input.contextRequest
107
+ ? { requestedContext: structuredClone(input.contextRequest) }
108
+ : {}),
109
+ ...(input.recoveryFeedback
110
+ ? { recoveryFeedback: structuredClone(input.recoveryFeedback) }
111
+ : {}),
112
+ ...(input.repair
113
+ ? {
114
+ repair: {
115
+ attempt: input.repair.attempt,
116
+ attribution: input.repair.attribution,
117
+ failureIdentities: [...input.repair.failureIdentities],
118
+ verification: structuredClone(input.task.repairVerification),
119
+ inBoundaryOnly: true,
120
+ },
121
+ }
122
+ : {}),
123
+ };
124
+ const child = await runChildSession({
125
+ cwd: input.workspaceRoot,
126
+ modelRuntime: phaseRuntime.modelRuntime,
127
+ model: phaseRuntime.model,
128
+ systemPrompt: [
129
+ implementationAgent.content,
130
+ input.task.objective,
131
+ input.task.context.agents,
132
+ input.task.context.contract,
133
+ "Use recoveryFeedback to change the failing approach. For compact-patch, prefer exact replace operations and omit unchanged bodies; submit one complete atomic patch. Never repeat an unchanged failing submission or weaken verification to obtain a pass.",
134
+ `<phase-contract>${JSON.stringify(phaseContract)}</phase-contract>`,
135
+ ].join("\n\n"),
136
+ requestId: childRequestId(input.operationId, input.taskId, input.phase),
137
+ taskId: input.taskId,
138
+ role: "implementation-worker",
139
+ phase: input.phase,
140
+ output: "diff",
141
+ roots: input.task.roots.map((root) =>
142
+ path.resolve(input.workspaceRoot, root),
143
+ ),
144
+ allowedPaths: [
145
+ ...new Set([
146
+ ...executionBoundary.read,
147
+ ...executionBoundary.write,
148
+ ...executionBoundary.delete,
149
+ ]),
150
+ ],
151
+ timeoutMs: LIMITS.phaseTimeoutMs,
152
+ signal: input.signal,
153
+ ledgerProjection: input.ledgerProjection,
154
+ candidateArtifact: input.candidateArtifact,
155
+ onStreamStart: input.onRequestStart,
156
+ onStreamHeaders: input.onHeaders,
157
+ onStreamProgress: input.onProgress,
158
+ });
159
+ if (!child.ok) {
160
+ if (child.failure.kind === "transport") {
161
+ throw transportFailureError(child.failure.code);
162
+ }
163
+ if (child.failure.kind === "cancelled") {
164
+ return { kind: "operation-cancelled", code: "cancelled" };
165
+ }
166
+ if (child.failure.kind === "approval-boundary") {
167
+ return { kind: "approval-needed", code: child.failure.code };
168
+ }
169
+ if (child.failure.kind === "execution-limit") {
170
+ return { kind: "paused", code: "needs-task-split" };
171
+ }
172
+ if (child.failure.kind === "result-limit") {
173
+ return { kind: "retryable", code: "needs-task-split" };
174
+ }
175
+ const attemptDiagnostic = {
176
+ finalCategory: child.classification.finalCategory,
177
+ submitAttempts: child.classification.attempts,
178
+ schema: child.classification.schema,
179
+ identityMismatch: Object.entries(child.classification.identity)
180
+ .filter(([, matches]) => !matches)
181
+ .map(([dimension]) => dimension),
182
+ };
183
+ return child.failure.kind === "environment" ||
184
+ child.failure.kind === "verification-adapter"
185
+ ? { kind: "paused", code: child.failure.code, attemptDiagnostic }
186
+ : { kind: "retryable", code: child.failure.code, attemptDiagnostic };
187
+ }
188
+ const result = child.result;
189
+ if (result.kind === "context-request") {
190
+ return classifyCandidateContextRequest(result, {
191
+ phase: input.phase,
192
+ contextReadRoots: input.task.roots,
193
+ readPaths: phase.read,
194
+ writePaths: phase.write,
195
+ deletePaths: phase.delete,
196
+ taskPaths: [
197
+ ...new Set(
198
+ Object.values(input.task.phases).flatMap((boundary) => [
199
+ ...boundary.read,
200
+ ...boundary.write,
201
+ ...boundary.delete,
202
+ ]),
203
+ ),
204
+ ],
205
+ redWritePaths: input.task.phases.red.write,
206
+ agents: {
207
+ impact: input.task.agents.impact,
208
+ ...(input.task.agents.target
209
+ ? { target: input.task.agents.target }
210
+ : {}),
211
+ },
212
+ });
213
+ }
214
+ if (result.kind !== "sealed-candidate") {
215
+ return { kind: "retryable", code: "candidate-diff-invalid" };
216
+ }
217
+ return {
218
+ kind: "sealed-candidate",
219
+ candidateId: result.candidateId,
220
+ artifactHash: result.artifactHash,
221
+ bytes: result.bytes,
222
+ paths: [...result.paths],
223
+ };
224
+ }