@abelxiaoxing/cadence 1.4.0 → 1.5.0
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 +130 -8
- package/agents/design-explorer.md +17 -11
- package/agents/diagnosis-worker.md +9 -1
- package/agents/implementation-worker.md +3 -1
- package/config/plan-draft.example.json +122 -0
- package/config/plan-draft.multiple-tasks.example.json +345 -0
- package/config/plan-draft.quick.example.json +78 -0
- package/package.json +10 -4
- package/prompts/abel-design.md +101 -44
- package/prompts/abel-implement.md +9 -0
- package/src/activity-contracts.ts +82 -0
- package/src/candidate-artifact.ts +143 -0
- package/src/candidate-patch.ts +31 -11
- package/src/candidate-workspace.ts +74 -0
- package/src/change-contract-schema.ts +191 -0
- package/src/change-contract.ts +5 -0
- package/src/change-verification.ts +920 -0
- package/src/child-budget.ts +58 -0
- package/src/child-model.ts +142 -0
- package/src/child-session.ts +289 -326
- package/src/contracts.ts +200 -76
- package/src/delivery-compiler.ts +105 -120
- package/src/delivery-revision.ts +42 -0
- package/src/design-control.ts +113 -4
- package/src/design-diagnostics.ts +150 -0
- package/src/durable-contracts.ts +213 -0
- package/src/durable-workflow.ts +148 -3570
- package/src/evidence-draft.ts +122 -0
- package/src/execution-profile.ts +38 -0
- package/src/implement-graph.ts +3 -0
- package/src/implement-plan.ts +70 -0
- package/src/index.ts +59 -522
- package/src/isolation-backend.ts +62 -40
- package/src/model-source.ts +22 -0
- package/src/operator-cli-entry.mjs +24 -0
- package/src/operator-cli.mjs +1421 -0
- package/src/operator-tools.ts +249 -0
- package/src/package-candidate.ts +227 -0
- package/src/package-verification.ts +86 -8
- package/src/package-workflow.ts +291 -0
- package/src/packet-runtime.ts +27 -48
- package/src/parent-provider.ts +86 -198
- package/src/phase-execution.ts +2248 -0
- package/src/pi-adapter.ts +19 -0
- package/src/plan-draft-summary.ts +149 -0
- package/src/plan-draft.ts +493 -0
- package/src/single-task-draft.ts +142 -0
- package/src/subagent-activity.ts +32 -66
- package/src/submit-tool.ts +20 -59
- package/src/transport-budget.ts +52 -0
- package/src/verification-capability.ts +5 -3
- package/src/verification-diagnostics.ts +76 -0
- package/src/verification-environment-io.ts +280 -0
- package/src/verification-environment.ts +56 -154
- package/src/verification-scan.ts +64 -0
- package/src/worker-broker.ts +39 -57
- package/src/workflow-policy.ts +14 -53
- package/src/workflow-recovery-policy.ts +95 -0
- package/src/workflow-scheduling.ts +105 -0
- package/src/workflow-state-machine.ts +105 -497
- package/src/workflow-status.ts +384 -0
- package/src/workspace-io-entry.mjs +19 -1
- package/src/workspace-io-worker.mjs +275 -50
- package/src/workspace-io.ts +6 -1
- package/src/empty-resource-loader.ts +0 -38
- package/src/parent-payload-bridge.ts +0 -523
package/src/delivery-compiler.ts
CHANGED
|
@@ -2,14 +2,12 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { compareCanonicalStrings } from "./canonical.ts";
|
|
3
3
|
import {
|
|
4
4
|
assertPlanWithinChangeContract,
|
|
5
|
-
type ChangeContract,
|
|
6
5
|
normalizeChangeContract,
|
|
7
6
|
} from "./change-contract.ts";
|
|
8
7
|
import {
|
|
9
8
|
type AgentsImpact,
|
|
10
9
|
type ImplementGraphBoundary,
|
|
11
10
|
type ImplementGraphOutput,
|
|
12
|
-
type ImplementTaskBoundary,
|
|
13
11
|
isAgentsPath,
|
|
14
12
|
isValidRelativePath,
|
|
15
13
|
type PhaseBoundary,
|
|
@@ -17,12 +15,25 @@ import {
|
|
|
17
15
|
validateImplementGraphBoundary,
|
|
18
16
|
validateVerificationContract,
|
|
19
17
|
} from "./contracts.ts";
|
|
18
|
+
import {
|
|
19
|
+
type DesignPlanDiagnostic,
|
|
20
|
+
DesignPlanValidationError,
|
|
21
|
+
} from "./design-diagnostics.ts";
|
|
20
22
|
import {
|
|
21
23
|
assessImplementGraphReadiness,
|
|
22
24
|
canonicalJson,
|
|
23
25
|
hashCanonicalValue,
|
|
24
26
|
type ImplementGraphReadiness,
|
|
25
27
|
} from "./implement-graph.ts";
|
|
28
|
+
import type {
|
|
29
|
+
ImplementPlan,
|
|
30
|
+
PlanAgentsCheckpointOperation,
|
|
31
|
+
PlanTaskDraft,
|
|
32
|
+
PlanTracking,
|
|
33
|
+
PlanVerification,
|
|
34
|
+
} from "./implement-plan.ts";
|
|
35
|
+
import { expandPlanDraft, preparePlanDraft } from "./plan-draft.ts";
|
|
36
|
+
import { expandSingleTaskDraft } from "./single-task-draft.ts";
|
|
26
37
|
import {
|
|
27
38
|
bindDraftVerificationInputs,
|
|
28
39
|
validateVerificationAdapterCapability,
|
|
@@ -103,40 +114,6 @@ export class DeliveryValidationError extends Error {
|
|
|
103
114
|
}
|
|
104
115
|
}
|
|
105
116
|
|
|
106
|
-
export interface DesignPlanDiagnostic {
|
|
107
|
-
code: string;
|
|
108
|
-
taskId?: string;
|
|
109
|
-
phase?: string;
|
|
110
|
-
field?: string;
|
|
111
|
-
category?: string;
|
|
112
|
-
owner?: string;
|
|
113
|
-
verificationId?: string;
|
|
114
|
-
outputId?: string;
|
|
115
|
-
dependencyTaskId?: string;
|
|
116
|
-
producerTaskId?: string;
|
|
117
|
-
producerPhase?: string;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export class DesignPlanValidationError extends Error {
|
|
121
|
-
readonly diagnostics: readonly DesignPlanDiagnostic[];
|
|
122
|
-
|
|
123
|
-
constructor(message: string, diagnostics: readonly DesignPlanDiagnostic[]) {
|
|
124
|
-
const normalized = [
|
|
125
|
-
...new Map(
|
|
126
|
-
diagnostics
|
|
127
|
-
.map((diagnostic) => structuredClone(diagnostic))
|
|
128
|
-
.sort((left, right) =>
|
|
129
|
-
compareCanonicalStrings(canonicalJson(left), canonicalJson(right)),
|
|
130
|
-
)
|
|
131
|
-
.map((diagnostic) => [canonicalJson(diagnostic), diagnostic]),
|
|
132
|
-
).values(),
|
|
133
|
-
];
|
|
134
|
-
super(message);
|
|
135
|
-
this.name = "DesignPlanValidationError";
|
|
136
|
-
this.diagnostics = Object.freeze(normalized);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
117
|
const SAFE_PLAN_ERROR_CODE = /^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$/u;
|
|
141
118
|
|
|
142
119
|
function planErrorCode(error: unknown): string {
|
|
@@ -182,6 +159,22 @@ export function assessDeliveryTraceability(input: {
|
|
|
182
159
|
plan: ImplementPlan;
|
|
183
160
|
}): DeliveryTraceabilityAssessment {
|
|
184
161
|
const diagnostics = new Set<string>();
|
|
162
|
+
let authorMarkdown = input.tasksMarkdown;
|
|
163
|
+
let bindings: string | undefined;
|
|
164
|
+
try {
|
|
165
|
+
const region = taskVerificationRegion(input.tasksMarkdown);
|
|
166
|
+
if (region) {
|
|
167
|
+
authorMarkdown =
|
|
168
|
+
input.tasksMarkdown.slice(0, region.start) +
|
|
169
|
+
input.tasksMarkdown.slice(region.end);
|
|
170
|
+
bindings = region.text;
|
|
171
|
+
if (bindings !== renderTasks(input.plan)) {
|
|
172
|
+
diagnostics.add("traceability-managed-region-stale");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
return { ok: false, diagnostics: ["traceability-managed-region-invalid"] };
|
|
177
|
+
}
|
|
185
178
|
const headings = new Set<string>();
|
|
186
179
|
for (const spec of input.specs) {
|
|
187
180
|
if (!isValidRelativePath(spec.path) || !spec.path.endsWith("/spec.md")) {
|
|
@@ -206,7 +199,7 @@ export function assessDeliveryTraceability(input: {
|
|
|
206
199
|
}
|
|
207
200
|
}
|
|
208
201
|
const references = [
|
|
209
|
-
...
|
|
202
|
+
...authorMarkdown.matchAll(/`(specs\/[^`\n]+\/spec\.md#[^`\n]+)`/gu),
|
|
210
203
|
].flatMap((match) => (match[1] ? [match[1].trim()] : []));
|
|
211
204
|
if (references.length === 0) diagnostics.add("traceability-reference-empty");
|
|
212
205
|
const referenceSet = new Set(references);
|
|
@@ -220,9 +213,7 @@ export function assessDeliveryTraceability(input: {
|
|
|
220
213
|
diagnostics.add("traceability-scenario-unowned");
|
|
221
214
|
}
|
|
222
215
|
const taskIds = input.plan.tasks.map((task) => task.taskId).sort();
|
|
223
|
-
if (
|
|
224
|
-
taskIds.some((taskId) => !input.tasksMarkdown.includes(`\`${taskId}\``))
|
|
225
|
-
) {
|
|
216
|
+
if (taskIds.some((taskId) => !authorMarkdown.includes(`\`${taskId}\``))) {
|
|
226
217
|
diagnostics.add("traceability-task-unmapped");
|
|
227
218
|
}
|
|
228
219
|
const verificationIds = input.plan.tasks
|
|
@@ -236,7 +227,8 @@ export function assessDeliveryTraceability(input: {
|
|
|
236
227
|
.sort();
|
|
237
228
|
if (
|
|
238
229
|
verificationIds.some(
|
|
239
|
-
(verificationId) =>
|
|
230
|
+
(verificationId) =>
|
|
231
|
+
!(bindings ?? authorMarkdown).includes(verificationId),
|
|
240
232
|
)
|
|
241
233
|
) {
|
|
242
234
|
diagnostics.add("traceability-verification-unmapped");
|
|
@@ -260,70 +252,16 @@ export function assessDeliveryTraceability(input: {
|
|
|
260
252
|
};
|
|
261
253
|
}
|
|
262
254
|
|
|
263
|
-
export type
|
|
264
|
-
|
|
265
|
-
export
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export interface PlanVerification {
|
|
276
|
-
baseline: {
|
|
277
|
-
target: "task-red-contracts";
|
|
278
|
-
affected: "task-affected-contracts";
|
|
279
|
-
fullSuite: StructuredVerificationContract;
|
|
280
|
-
failureIdentity: "normalized";
|
|
281
|
-
};
|
|
282
|
-
change: {
|
|
283
|
-
affected: "task-affected-contracts";
|
|
284
|
-
fullSuite: StructuredVerificationContract;
|
|
285
|
-
postApply: StructuredVerificationContract;
|
|
286
|
-
};
|
|
287
|
-
artifactCorrection: {
|
|
288
|
-
maxAttempts: number;
|
|
289
|
-
};
|
|
290
|
-
repair: {
|
|
291
|
-
maxAttempts: number;
|
|
292
|
-
inBoundaryOnly: true;
|
|
293
|
-
approvalOnBoundaryExpansion: true;
|
|
294
|
-
attribution: ["pre-existing", "introduced", "unresolved", "environment"];
|
|
295
|
-
};
|
|
296
|
-
agentsCheckpoint: {
|
|
297
|
-
required: boolean;
|
|
298
|
-
verification: StructuredVerificationContract | null;
|
|
299
|
-
operations: PlanAgentsCheckpointOperation[];
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
export interface PlanAgentsCheckpointOperation {
|
|
304
|
-
target: string;
|
|
305
|
-
impact: Exclude<AgentsImpact, "none">;
|
|
306
|
-
taskIds: string[];
|
|
307
|
-
managedBlock: string | null;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
export interface PlanTracking {
|
|
311
|
-
path: "tasks.md";
|
|
312
|
-
format: "markdown-checkbox";
|
|
313
|
-
taskIds: string[];
|
|
314
|
-
completionOwner: "parent";
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
export interface PlanDraft {
|
|
318
|
-
changeContract?: ChangeContract;
|
|
319
|
-
changeId: string;
|
|
320
|
-
tasks: PlanTaskDraft[];
|
|
321
|
-
outputs: ImplementGraphOutput[];
|
|
322
|
-
verification: PlanVerification;
|
|
323
|
-
tracking: PlanTracking;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
export type ImplementPlan = PlanDraft;
|
|
255
|
+
export type { DesignPlanDiagnostic } from "./design-diagnostics.ts";
|
|
256
|
+
export { DesignPlanValidationError } from "./design-diagnostics.ts";
|
|
257
|
+
export type {
|
|
258
|
+
ImplementPlan,
|
|
259
|
+
PlanAgentsCheckpointOperation,
|
|
260
|
+
PlanTaskDraft,
|
|
261
|
+
PlanTracking,
|
|
262
|
+
PlanVerification,
|
|
263
|
+
} from "./implement-plan.ts";
|
|
264
|
+
export type { PlanDraft, PlanPhaseDraft, PlanTaskInput } from "./plan-draft.ts";
|
|
327
265
|
|
|
328
266
|
export interface CompiledDelivery {
|
|
329
267
|
plan: ImplementPlan;
|
|
@@ -690,7 +628,7 @@ function normalizeVerification(
|
|
|
690
628
|
return cloned;
|
|
691
629
|
}
|
|
692
630
|
|
|
693
|
-
function normalizePhase(value: unknown, readOnly = false):
|
|
631
|
+
function normalizePhase(value: unknown, readOnly = false): PhaseBoundary {
|
|
694
632
|
if (
|
|
695
633
|
!isRecord(value) ||
|
|
696
634
|
!exactKeys(value, [
|
|
@@ -837,7 +775,7 @@ function normalizeTask(value: unknown): PlanTaskDraft {
|
|
|
837
775
|
}
|
|
838
776
|
|
|
839
777
|
function graphFromPlan(plan: ImplementPlan): ImplementGraphBoundary {
|
|
840
|
-
const stripPhase = (phase:
|
|
778
|
+
const stripPhase = (phase: PhaseBoundary): PhaseBoundary => ({
|
|
841
779
|
read: phase.read,
|
|
842
780
|
write: phase.write,
|
|
843
781
|
delete: phase.delete,
|
|
@@ -1177,7 +1115,7 @@ function normalizeDraft(value: unknown): ImplementPlan {
|
|
|
1177
1115
|
if (!draftGraph.ok) {
|
|
1178
1116
|
throw new DesignPlanValidationError(
|
|
1179
1117
|
`delivery-plan-invalid:${draftGraph.reason}`,
|
|
1180
|
-
[draftGraph.diagnostic],
|
|
1118
|
+
draftGraph.diagnostics ?? [draftGraph.diagnostic],
|
|
1181
1119
|
);
|
|
1182
1120
|
}
|
|
1183
1121
|
const plan: ImplementPlan = {
|
|
@@ -1247,32 +1185,79 @@ function normalizeDraft(value: unknown): ImplementPlan {
|
|
|
1247
1185
|
return plan;
|
|
1248
1186
|
}
|
|
1249
1187
|
|
|
1188
|
+
const TASK_BINDINGS_START = "<!-- ABEL:VERIFICATION-BINDINGS:START -->";
|
|
1189
|
+
const TASK_BINDINGS_END = "<!-- ABEL:VERIFICATION-BINDINGS:END -->";
|
|
1190
|
+
|
|
1191
|
+
function taskVerificationRegion(markdown: string) {
|
|
1192
|
+
const markers = markdown.match(/<!-- ABEL:VERIFICATION-BINDINGS/gu) ?? [];
|
|
1193
|
+
if (markers.length === 0) return undefined;
|
|
1194
|
+
const match =
|
|
1195
|
+
/^<!-- ABEL:VERIFICATION-BINDINGS:START -->\r?\n[\s\S]*?^<!-- ABEL:VERIFICATION-BINDINGS:END -->(?=\r?$)/mu.exec(
|
|
1196
|
+
markdown,
|
|
1197
|
+
);
|
|
1198
|
+
if (markers.length !== 2 || !match) {
|
|
1199
|
+
throw new DesignPlanValidationError("traceability-managed-region-invalid", [
|
|
1200
|
+
{ code: "traceability-managed-region-invalid", path: "tasks.md" },
|
|
1201
|
+
]);
|
|
1202
|
+
}
|
|
1203
|
+
return {
|
|
1204
|
+
start: match.index,
|
|
1205
|
+
end: match.index + match[0].length,
|
|
1206
|
+
text: match[0],
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Only identity bindings are generated. Author task checkboxes and Scenario
|
|
1211
|
+
// ownership remain outside this region and are validated independently.
|
|
1250
1212
|
function renderTasks(plan: ImplementPlan): string {
|
|
1251
|
-
const lines = ["
|
|
1252
|
-
for (const task of plan.tasks)
|
|
1253
|
-
|
|
1213
|
+
const lines = [TASK_BINDINGS_START, "## Compiled verification bindings", ""];
|
|
1214
|
+
for (const task of [...plan.tasks].sort((a, b) =>
|
|
1215
|
+
compareCanonicalStrings(a.taskId, b.taskId),
|
|
1216
|
+
)) {
|
|
1254
1217
|
lines.push(
|
|
1255
|
-
|
|
1218
|
+
`- Task \`${task.taskId}\` (${task.verificationMode ?? "behavior"})`,
|
|
1256
1219
|
);
|
|
1257
|
-
if (!task.verificationMode || task.verificationMode === "behavior")
|
|
1258
|
-
lines.push(` - Red
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
);
|
|
1263
|
-
|
|
1220
|
+
if (!task.verificationMode || task.verificationMode === "behavior") {
|
|
1221
|
+
lines.push(` - Red: \`${task.phases.red.verification.id}\``);
|
|
1222
|
+
}
|
|
1223
|
+
lines.push(` - Green: \`${task.phases.green.verification.id}\``);
|
|
1224
|
+
if (task.phases.refactor) {
|
|
1225
|
+
lines.push(` - Refactor: \`${task.phases.refactor.verification.id}\``);
|
|
1226
|
+
}
|
|
1264
1227
|
}
|
|
1265
|
-
lines.push(
|
|
1228
|
+
lines.push(TASK_BINDINGS_END);
|
|
1266
1229
|
return lines.join("\n");
|
|
1267
1230
|
}
|
|
1268
1231
|
|
|
1232
|
+
export function bindTaskVerifications(
|
|
1233
|
+
markdown: string,
|
|
1234
|
+
plan: ImplementPlan,
|
|
1235
|
+
): string {
|
|
1236
|
+
const region = taskVerificationRegion(markdown);
|
|
1237
|
+
const bindings = renderTasks(plan);
|
|
1238
|
+
if (region) {
|
|
1239
|
+
return (
|
|
1240
|
+
markdown.slice(0, region.start) + bindings + markdown.slice(region.end)
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
return `${markdown}${markdown.endsWith("\n") ? "\n" : "\n\n"}${bindings}\n`;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1269
1246
|
export function compileImplementPlan(
|
|
1270
1247
|
draft: unknown,
|
|
1271
1248
|
options: { consumerRoot: string; bindExecutionInputs?: boolean },
|
|
1272
1249
|
): CompiledDelivery {
|
|
1273
1250
|
try {
|
|
1251
|
+
draft = expandSingleTaskDraft(draft);
|
|
1252
|
+
// Resolve omitted manifest command bytes before named definitions are
|
|
1253
|
+
// validated and purpose identities are hashed. The second pass below
|
|
1254
|
+
// binds execution inputs on the expanded contracts with their final IDs.
|
|
1255
|
+
if (options.bindExecutionInputs)
|
|
1256
|
+
draft = bindDraftVerificationInputs(options.consumerRoot, draft);
|
|
1257
|
+
draft = expandPlanDraft(draft);
|
|
1274
1258
|
if (options.bindExecutionInputs)
|
|
1275
1259
|
draft = bindDraftVerificationInputs(options.consumerRoot, draft);
|
|
1260
|
+
draft = preparePlanDraft(draft);
|
|
1276
1261
|
const normalized = normalizeDraft(draft);
|
|
1277
1262
|
const plan =
|
|
1278
1263
|
isRecord(draft) && legacyPlans.get(draft) === JSON.stringify(draft)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ImplementPlan } from "./implement-plan.ts";
|
|
2
|
+
import {
|
|
3
|
+
deliveryBoundPaths,
|
|
4
|
+
type EngineTaskRow,
|
|
5
|
+
taskApprovalBoundary,
|
|
6
|
+
} from "./workflow-policy.ts";
|
|
7
|
+
/** Compare authority only; retained bytes still require executor currentness and replay. */
|
|
8
|
+
export function compareDeliveryRevision(
|
|
9
|
+
priorPlan: ImplementPlan | undefined,
|
|
10
|
+
nextPlan: ImplementPlan,
|
|
11
|
+
priorRows: readonly Pick<EngineTaskRow, "task_id">[],
|
|
12
|
+
) {
|
|
13
|
+
const priorTasks = new Map(
|
|
14
|
+
(priorPlan?.tasks ?? []).map((task) => [task.taskId, task]),
|
|
15
|
+
);
|
|
16
|
+
const nextTasks = new Map(nextPlan.tasks.map((task) => [task.taskId, task]));
|
|
17
|
+
const priorBoundPaths = new Set(
|
|
18
|
+
priorPlan ? deliveryBoundPaths(priorPlan) : [],
|
|
19
|
+
);
|
|
20
|
+
const boundaryExpanded = deliveryBoundPaths(nextPlan).some(
|
|
21
|
+
(relative) => !priorBoundPaths.has(relative),
|
|
22
|
+
);
|
|
23
|
+
const compatible = new Set<string>();
|
|
24
|
+
if (priorPlan) {
|
|
25
|
+
for (const [taskId, priorTask] of priorTasks) {
|
|
26
|
+
const nextTask = nextTasks.get(taskId);
|
|
27
|
+
if (
|
|
28
|
+
nextTask &&
|
|
29
|
+
taskApprovalBoundary(priorPlan, priorTask) ===
|
|
30
|
+
taskApprovalBoundary(nextPlan, nextTask)
|
|
31
|
+
) {
|
|
32
|
+
compatible.add(taskId);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const invalidated = new Set(
|
|
37
|
+
priorRows
|
|
38
|
+
.filter((row) => !compatible.has(row.task_id))
|
|
39
|
+
.map((row) => row.task_id),
|
|
40
|
+
);
|
|
41
|
+
return { compatible, invalidated, boundaryExpanded };
|
|
42
|
+
}
|
package/src/design-control.ts
CHANGED
|
@@ -13,9 +13,11 @@ import {
|
|
|
13
13
|
type ChangeContract,
|
|
14
14
|
normalizeChangeContract,
|
|
15
15
|
} from "./change-contract.ts";
|
|
16
|
+
import { changeContractDiagnostics } from "./change-contract-schema.ts";
|
|
16
17
|
import { isValidRelativePath } from "./contracts.ts";
|
|
17
18
|
import {
|
|
18
19
|
assessDeliveryTraceability,
|
|
20
|
+
bindTaskVerifications,
|
|
19
21
|
compileGateAReceipt,
|
|
20
22
|
compileImplementPlan,
|
|
21
23
|
compileReadyReceipt,
|
|
@@ -26,6 +28,7 @@ import {
|
|
|
26
28
|
parseImplementPlan,
|
|
27
29
|
parseReadyReceipt,
|
|
28
30
|
} from "./delivery-compiler.ts";
|
|
31
|
+
import type { DesignPlanDiagnostic } from "./design-diagnostics.ts";
|
|
29
32
|
import {
|
|
30
33
|
type DesignArtifactOperationFacts,
|
|
31
34
|
type DesignArtifactOperationOutcome,
|
|
@@ -33,6 +36,7 @@ import {
|
|
|
33
36
|
type DesignStatusProjection,
|
|
34
37
|
} from "./design-journal.ts";
|
|
35
38
|
import { OpenSpecCliError, type OpenSpecDiagnostic } from "./openspec-cli.ts";
|
|
39
|
+
import { planAuthoringSummary } from "./plan-draft-summary.ts";
|
|
36
40
|
import { canonicalJson } from "./run-state.ts";
|
|
37
41
|
import { RunStore } from "./run-store.ts";
|
|
38
42
|
import { observeSafePath } from "./safe-path.ts";
|
|
@@ -199,7 +203,53 @@ function lifecycleOperationId(kind: string, runId: string): string {
|
|
|
199
203
|
.slice(0, 32)}`;
|
|
200
204
|
}
|
|
201
205
|
|
|
202
|
-
export
|
|
206
|
+
export class DesignControlValidationError extends DesignPlanValidationError {
|
|
207
|
+
constructor(diagnostics: readonly DesignPlanDiagnostic[]) {
|
|
208
|
+
super("invalid-design-control-request", diagnostics);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
export function validateDesignControlRequest(value: unknown):
|
|
212
|
+
| { ok: true; value: DesignControlRequest }
|
|
213
|
+
| {
|
|
214
|
+
ok: false;
|
|
215
|
+
code: "invalid-design-control-request";
|
|
216
|
+
diagnostics: DesignPlanDiagnostic[];
|
|
217
|
+
} {
|
|
218
|
+
const parsed = parseDesignControlRequest(value);
|
|
219
|
+
if (parsed.ok) return parsed;
|
|
220
|
+
let diagnostics: DesignPlanDiagnostic[] = [];
|
|
221
|
+
if (
|
|
222
|
+
isRecord(value) &&
|
|
223
|
+
value.operation === "approve-gate" &&
|
|
224
|
+
value.gate === "gate-a" &&
|
|
225
|
+
typeof value.contract !== "string"
|
|
226
|
+
)
|
|
227
|
+
diagnostics = changeContractDiagnostics(value.contract);
|
|
228
|
+
if (!diagnostics.length) {
|
|
229
|
+
const field = !isRecord(value)
|
|
230
|
+
? "request"
|
|
231
|
+
: typeof value.operation !== "string"
|
|
232
|
+
? "operation"
|
|
233
|
+
: value.operation !== "start" &&
|
|
234
|
+
(typeof value.runId !== "string" || !IDENTIFIER.test(value.runId))
|
|
235
|
+
? "runId"
|
|
236
|
+
: !["status", "validate-plan-draft"].includes(
|
|
237
|
+
String(value.operation),
|
|
238
|
+
) &&
|
|
239
|
+
(typeof value.operationId !== "string" ||
|
|
240
|
+
!IDENTIFIER.test(value.operationId))
|
|
241
|
+
? "operationId"
|
|
242
|
+
: value.operation === "approve-gate"
|
|
243
|
+
? value.gate === "gate-a"
|
|
244
|
+
? "contract"
|
|
245
|
+
: "gate"
|
|
246
|
+
: "request";
|
|
247
|
+
diagnostics = [{ code: "design-control-field-invalid", field }];
|
|
248
|
+
}
|
|
249
|
+
return { ...parsed, diagnostics };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function parseDesignControlRequest(
|
|
203
253
|
value: unknown,
|
|
204
254
|
):
|
|
205
255
|
| { ok: true; value: DesignControlRequest }
|
|
@@ -553,7 +603,8 @@ export class DesignController {
|
|
|
553
603
|
async execute(value: unknown): Promise<Record<string, unknown>> {
|
|
554
604
|
this.#assertOpen();
|
|
555
605
|
const validation = validateDesignControlRequest(value);
|
|
556
|
-
if (!validation.ok)
|
|
606
|
+
if (!validation.ok)
|
|
607
|
+
throw new DesignControlValidationError(validation.diagnostics);
|
|
557
608
|
const request = validation.value;
|
|
558
609
|
switch (request.operation) {
|
|
559
610
|
case "start":
|
|
@@ -631,6 +682,30 @@ export class DesignController {
|
|
|
631
682
|
completed: run.state === "completed",
|
|
632
683
|
...(run.pauseCode ? { pause: { code: run.pauseCode } } : {}),
|
|
633
684
|
legalOperations,
|
|
685
|
+
nextStep: terminal
|
|
686
|
+
? null
|
|
687
|
+
: !design.gates.gateA.current
|
|
688
|
+
? {
|
|
689
|
+
operation: "approve-gate",
|
|
690
|
+
instruction:
|
|
691
|
+
"Read applicable instructions, the affected source and existing tests, and the verification manifest together. Resolve only remaining substantive choices; use existing same-task approval. Submit the accepted structured ChangeContract, not a prose placeholder.",
|
|
692
|
+
}
|
|
693
|
+
: !design.change
|
|
694
|
+
? {
|
|
695
|
+
operation: "bind-change",
|
|
696
|
+
instruction: "Bind the approved change name to this runId.",
|
|
697
|
+
}
|
|
698
|
+
: !design.plan || !design.gates.gateB.current
|
|
699
|
+
? {
|
|
700
|
+
operation: "write-artifact",
|
|
701
|
+
instruction:
|
|
702
|
+
"Write proposal, design, specs, author task checkboxes with exact Scenario references, and plan-draft.json. Use the installed example named in the tool description; omit derived identities. Preflight then compile; compilation supplies verification bindings and Gate B.",
|
|
703
|
+
}
|
|
704
|
+
: {
|
|
705
|
+
operation: "finalize-delivery",
|
|
706
|
+
instruction:
|
|
707
|
+
"Finalize the prepared artifacts. If tasks or the draft changed since compilation, preflight and compile again first. No separate Gate B approval is needed.",
|
|
708
|
+
},
|
|
634
709
|
packetActions: terminal ? [] : ["finish"],
|
|
635
710
|
design,
|
|
636
711
|
};
|
|
@@ -866,6 +941,7 @@ export class DesignController {
|
|
|
866
941
|
{ code: "design-plan-draft-invalid", field: "plan-draft.json" },
|
|
867
942
|
]);
|
|
868
943
|
}
|
|
944
|
+
const authoredDraft = structuredClone(draft);
|
|
869
945
|
if (status.changeContract) {
|
|
870
946
|
if (!draft || typeof draft !== "object" || Array.isArray(draft))
|
|
871
947
|
throw new Error("change-contract-invalid");
|
|
@@ -888,7 +964,7 @@ export class DesignController {
|
|
|
888
964
|
{ code: "design-plan-change-mismatch", field: "changeId" },
|
|
889
965
|
]);
|
|
890
966
|
}
|
|
891
|
-
return { status, compiled };
|
|
967
|
+
return { status, compiled, authoredDraft };
|
|
892
968
|
}
|
|
893
969
|
|
|
894
970
|
#validatePlanDraft(
|
|
@@ -897,11 +973,12 @@ export class DesignController {
|
|
|
897
973
|
{ operation: "validate-plan-draft" }
|
|
898
974
|
>,
|
|
899
975
|
) {
|
|
900
|
-
const { compiled } = this.#readCompiledDraft(request.runId);
|
|
976
|
+
const { compiled, authoredDraft } = this.#readCompiledDraft(request.runId);
|
|
901
977
|
return {
|
|
902
978
|
operation: request.operation,
|
|
903
979
|
runId: request.runId,
|
|
904
980
|
valid: true,
|
|
981
|
+
summary: planAuthoringSummary(compiled.plan, authoredDraft),
|
|
905
982
|
plan: {
|
|
906
983
|
taskCount: compiled.plan.tasks.length,
|
|
907
984
|
outputCount: compiled.plan.outputs.length,
|
|
@@ -931,7 +1008,29 @@ export class DesignController {
|
|
|
931
1008
|
if (!change) throw new Error("design-change-required");
|
|
932
1009
|
const readyPath = safeRelative(change, "ready.yaml");
|
|
933
1010
|
const gateAPath = safeRelative(change, "gate-a.yaml");
|
|
1011
|
+
const tasksPath = safeRelative(change, "tasks.md");
|
|
1012
|
+
if (observeSafePath(this.consumerRoot, tasksPath).kind === "absent") {
|
|
1013
|
+
throw new DesignPlanValidationError(
|
|
1014
|
+
"design-traceability-input-unavailable",
|
|
1015
|
+
[{ code: "design-traceability-input-unavailable", path: "tasks.md" }],
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
const priorTasks = boundedArtifactBytes(this.consumerRoot, tasksPath);
|
|
1019
|
+
const tasksBytes = Buffer.from(
|
|
1020
|
+
bindTaskVerifications(
|
|
1021
|
+
new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
|
|
1022
|
+
priorTasks,
|
|
1023
|
+
),
|
|
1024
|
+
compiled.plan,
|
|
1025
|
+
),
|
|
1026
|
+
"utf8",
|
|
1027
|
+
);
|
|
1028
|
+
if (tasksBytes.length > MAX_DESIGN_FILE_BYTES) {
|
|
1029
|
+
throw new Error("design-artifact-content-too-large");
|
|
1030
|
+
}
|
|
1031
|
+
const tasksUnchanged = tasksBytes.equals(priorTasks);
|
|
934
1032
|
const unchanged =
|
|
1033
|
+
tasksUnchanged &&
|
|
935
1034
|
status.gates.gateB.current &&
|
|
936
1035
|
status.plan?.rawSha256 === compiled.rawSha256 &&
|
|
937
1036
|
status.plan.canonicalHash === compiled.planHash;
|
|
@@ -940,6 +1039,16 @@ export class DesignController {
|
|
|
940
1039
|
removeSafeFile(this.consumerRoot, gateAPath);
|
|
941
1040
|
}
|
|
942
1041
|
this.#assertFinalizationLease(lease);
|
|
1042
|
+
if (
|
|
1043
|
+
!boundedArtifactBytes(this.consumerRoot, tasksPath).equals(priorTasks)
|
|
1044
|
+
) {
|
|
1045
|
+
throw new Error("design-artifact-currentness-mismatch");
|
|
1046
|
+
}
|
|
1047
|
+
// The compilation journal commits only after both files are installed.
|
|
1048
|
+
// A crash before that point can replay this deterministic projection;
|
|
1049
|
+
// finalization always rechecks the plan and task bytes together.
|
|
1050
|
+
if (!tasksUnchanged)
|
|
1051
|
+
atomicWrite(this.consumerRoot, tasksPath, tasksBytes);
|
|
943
1052
|
atomicWrite(
|
|
944
1053
|
this.consumerRoot,
|
|
945
1054
|
safeRelative(change, "implement-plan.json"),
|