@abelxiaoxing/cadence 1.2.1 → 1.2.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.
@@ -9,7 +9,9 @@ import {
9
9
  type CandidateFailure,
10
10
  type DiffResult,
11
11
  type EvidenceResult,
12
+ isAgentsPath,
12
13
  isValidRelativePath,
14
+ LIMITS,
13
15
  type SubmitFinalCategory,
14
16
  type SubmitSchemaState,
15
17
  validateDiffResult,
@@ -144,15 +146,57 @@ function candidateArtifactSchema(candidateId: string) {
144
146
  code: Type.Union([
145
147
  Type.Literal("approved-context-needed"),
146
148
  Type.Literal("task-split-needed"),
147
- Type.Literal("boundary-review-needed"),
148
149
  ]),
149
- refs: Type.Array(Type.String(), { maxItems: 32 }),
150
+ refs: Type.Array(
151
+ Type.Union([
152
+ Type.String(),
153
+ Type.Object(
154
+ {
155
+ kind: Type.Literal("requested-path"),
156
+ path: Type.String(),
157
+ access: Type.Union([
158
+ Type.Literal("read"),
159
+ Type.Literal("write"),
160
+ ]),
161
+ },
162
+ { additionalProperties: false },
163
+ ),
164
+ Type.Object(
165
+ {
166
+ kind: Type.Literal("source-citation"),
167
+ path: Type.String(),
168
+ line: Type.Integer({ minimum: 1 }),
169
+ },
170
+ { additionalProperties: false },
171
+ ),
172
+ Type.Object(
173
+ {
174
+ kind: Type.Literal("contract-diagnostic"),
175
+ ref: Type.String(),
176
+ },
177
+ { additionalProperties: false },
178
+ ),
179
+ ]),
180
+ { minItems: 1, maxItems: 32 },
181
+ ),
150
182
  },
151
183
  { additionalProperties: false },
152
184
  ),
153
185
  ]);
154
186
  }
155
187
 
188
+ export type CandidateContextRequestCode =
189
+ | "approved-context-needed"
190
+ | "task-split-needed"
191
+ | "boundary-review-needed";
192
+
193
+ export type CandidateContextRef =
194
+ | { kind: "requested-path"; path: string; access: "read" | "write" }
195
+ | { kind: "source-citation"; path: string; line: number }
196
+ | { kind: "contract-diagnostic"; ref: string };
197
+
198
+ export type CandidateContextRefInput = string | CandidateContextRef;
199
+
156
200
  export type CandidateArtifactSubmissionResult =
157
201
  | {
158
202
  kind: "sealed-candidate";
@@ -166,11 +210,8 @@ export type CandidateArtifactSubmissionResult =
166
210
  | {
167
211
  kind: "context-request";
168
212
  candidateId: string;
169
- code:
170
- | "approved-context-needed"
171
- | "task-split-needed"
172
- | "boundary-review-needed";
173
- refs: string[];
213
+ code: CandidateContextRequestCode;
214
+ refs: CandidateContextRef[];
174
215
  };
175
216
 
176
217
  export type CandidateContextRequest = Extract<
@@ -178,18 +219,114 @@ export type CandidateContextRequest = Extract<
178
219
  { kind: "context-request" }
179
220
  >;
180
221
 
222
+ export interface CandidateContextBoundary {
223
+ phase: "red" | "green" | "refactor";
224
+ readPaths: readonly string[];
225
+ writePaths: readonly string[];
226
+ deletePaths?: readonly string[];
227
+ taskPaths: readonly string[];
228
+ redWritePaths: readonly string[];
229
+ agents: {
230
+ impact: "none" | "update-existing" | "create-index" | "remove-index";
231
+ target?: string;
232
+ };
233
+ }
234
+
181
235
  export interface ClassifiedCandidateContextRequest {
182
- kind: "paused" | "approval-needed";
236
+ kind: "paused" | "retryable" | "approval-needed";
183
237
  code:
184
238
  | "approved-context-needed"
185
239
  | "task-split-needed"
186
- | "boundary-review-needed";
240
+ | "boundary-review-needed"
241
+ | "red-artifact-constraint"
242
+ | "agents-context-needed"
243
+ | "agents-write-parent-owned"
244
+ | "agents-contract-insufficient";
187
245
  contextRequest: {
188
- code: CandidateContextRequest["code"];
189
- refs: string[];
246
+ code: CandidateContextRequestCode;
247
+ refs: CandidateContextRef[];
190
248
  };
191
249
  }
192
250
 
251
+ const LEGACY_CITATION = /^(.*):(\d+)$/u;
252
+ const LEGACY_DIAGNOSTIC =
253
+ /^(?:phase-contract|task-contract|sealed-contract|workflow-contract)\.[a-z0-9._-]+$/iu;
254
+
255
+ function validDiagnosticRef(value: unknown): value is string {
256
+ return (
257
+ typeof value === "string" &&
258
+ value.length > 0 &&
259
+ value.length <= 512 &&
260
+ value.trim() === value &&
261
+ !/[\r\n\0]/u.test(value)
262
+ );
263
+ }
264
+
265
+ function normalizeCandidateContextRef(
266
+ input: CandidateContextRefInput,
267
+ ): CandidateContextRef {
268
+ if (typeof input === "string") {
269
+ const citation = LEGACY_CITATION.exec(input);
270
+ if (citation && isValidRelativePath(citation[1])) {
271
+ const line = Number(citation[2]);
272
+ if (Number.isSafeInteger(line) && line >= 1) {
273
+ return { kind: "source-citation", path: citation[1], line };
274
+ }
275
+ }
276
+ if (LEGACY_DIAGNOSTIC.test(input)) {
277
+ return { kind: "contract-diagnostic", ref: input };
278
+ }
279
+ if (!isValidRelativePath(input)) {
280
+ throw new Error("context request refs are invalid");
281
+ }
282
+ return { kind: "requested-path", path: input, access: "read" };
283
+ }
284
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
285
+ throw new Error("context request refs are invalid");
286
+ }
287
+ if (
288
+ input.kind === "requested-path" &&
289
+ Object.keys(input).length === 3 &&
290
+ isValidRelativePath(input.path) &&
291
+ (input.access === "read" || input.access === "write")
292
+ ) {
293
+ return { ...input };
294
+ }
295
+ if (
296
+ input.kind === "source-citation" &&
297
+ Object.keys(input).length === 3 &&
298
+ isValidRelativePath(input.path) &&
299
+ Number.isSafeInteger(input.line) &&
300
+ input.line >= 1
301
+ ) {
302
+ return { ...input };
303
+ }
304
+ if (
305
+ input.kind === "contract-diagnostic" &&
306
+ Object.keys(input).length === 2 &&
307
+ validDiagnosticRef(input.ref)
308
+ ) {
309
+ return { ...input };
310
+ }
311
+ throw new Error("context request refs are invalid");
312
+ }
313
+
314
+ export function normalizeCandidateContextRefs(
315
+ value: unknown,
316
+ ): CandidateContextRef[] {
317
+ if (!Array.isArray(value) || value.length === 0 || value.length > 32) {
318
+ throw new Error("context request refs are invalid");
319
+ }
320
+ const refs = value.map((entry) =>
321
+ normalizeCandidateContextRef(entry as CandidateContextRefInput),
322
+ );
323
+ const identities = refs.map((ref) => JSON.stringify(ref));
324
+ if (new Set(identities).size !== refs.length) {
325
+ throw new Error("context request refs are invalid");
326
+ }
327
+ return refs;
328
+ }
329
+
193
330
  function contextRefWithinBoundary(relative: string, approved: string): boolean {
194
331
  return (
195
332
  approved === "." ||
@@ -198,23 +335,108 @@ function contextRefWithinBoundary(relative: string, approved: string): boolean {
198
335
  );
199
336
  }
200
337
 
338
+ function normalizedBoundary(
339
+ boundary: readonly string[] | CandidateContextBoundary,
340
+ ): CandidateContextBoundary {
341
+ if (!Array.isArray(boundary)) return boundary as CandidateContextBoundary;
342
+ return {
343
+ phase: "green",
344
+ readPaths: boundary,
345
+ writePaths: [],
346
+ deletePaths: [],
347
+ taskPaths: boundary,
348
+ redWritePaths: [],
349
+ agents: { impact: "none" },
350
+ };
351
+ }
352
+
353
+ function pathWithinAny(relative: string, approved: readonly string[]): boolean {
354
+ return approved.some((path) => contextRefWithinBoundary(relative, path));
355
+ }
356
+
201
357
  export function classifyCandidateContextRequest(
202
- request: CandidateContextRequest,
203
- approvedPaths: readonly string[],
358
+ request: {
359
+ kind: "context-request";
360
+ candidateId: string;
361
+ code: CandidateContextRequestCode;
362
+ refs: readonly CandidateContextRefInput[];
363
+ },
364
+ inputBoundary: readonly string[] | CandidateContextBoundary,
204
365
  ): ClassifiedCandidateContextRequest {
205
- const refs = [...request.refs];
206
- const inBoundary =
207
- refs.length > 0 &&
208
- refs.every((relative) =>
209
- approvedPaths.some((approved) =>
210
- contextRefWithinBoundary(relative, approved),
366
+ const refs = normalizeCandidateContextRefs(request.refs);
367
+ const boundary = normalizedBoundary(inputBoundary);
368
+ const phasePaths = [
369
+ ...boundary.readPaths,
370
+ ...boundary.writePaths,
371
+ ...(boundary.deletePaths ?? []),
372
+ ];
373
+ const requested = refs.filter(
374
+ (ref): ref is Extract<CandidateContextRef, { kind: "requested-path" }> =>
375
+ ref.kind === "requested-path",
376
+ );
377
+ const ordinaryRequested = requested.filter((ref) => !isAgentsPath(ref.path));
378
+ const outsideOrdinary = ordinaryRequested.filter(
379
+ (ref) =>
380
+ !pathWithinAny(
381
+ ref.path,
382
+ ref.access === "write"
383
+ ? [...boundary.writePaths, ...(boundary.deletePaths ?? [])]
384
+ : phasePaths,
211
385
  ),
386
+ );
387
+ const agentsRequested = requested.filter((ref) => isAgentsPath(ref.path));
388
+ const agentsWrites = agentsRequested.filter((ref) => ref.access === "write");
389
+ const citedRedArtifacts = refs.filter(
390
+ (ref): ref is Extract<CandidateContextRef, { kind: "source-citation" }> =>
391
+ ref.kind === "source-citation" &&
392
+ pathWithinAny(ref.path, boundary.redWritePaths) &&
393
+ !pathWithinAny(ref.path, [
394
+ ...boundary.writePaths,
395
+ ...(boundary.deletePaths ?? []),
396
+ ]),
397
+ );
398
+ const requestedRedArtifacts = outsideOrdinary.filter(
399
+ (ref) =>
400
+ pathWithinAny(ref.path, boundary.redWritePaths) &&
401
+ pathWithinAny(ref.path, boundary.taskPaths),
402
+ );
403
+ const redArtifactConstraint =
404
+ boundary.phase === "green" &&
405
+ (citedRedArtifacts.length > 0 || requestedRedArtifacts.length > 0) &&
406
+ outsideOrdinary.every((ref) => requestedRedArtifacts.includes(ref)) &&
407
+ agentsRequested.every((ref) => ref.access === "read");
408
+
409
+ let classification: Pick<ClassifiedCandidateContextRequest, "kind" | "code">;
410
+ if (outsideOrdinary.length > 0 && !redArtifactConstraint) {
411
+ classification = {
412
+ kind: "approval-needed",
413
+ code: "boundary-review-needed",
414
+ };
415
+ } else if (agentsWrites.length > 0) {
416
+ const targetMismatch = agentsWrites.some(
417
+ (ref) =>
418
+ boundary.agents.impact === "none" ||
419
+ boundary.agents.target === undefined ||
420
+ ref.path !== boundary.agents.target,
212
421
  );
213
- const approvalNeeded =
214
- !inBoundary || request.code === "boundary-review-needed";
422
+ classification = targetMismatch
423
+ ? { kind: "approval-needed", code: "agents-contract-insufficient" }
424
+ : { kind: "paused", code: "agents-write-parent-owned" };
425
+ } else if (redArtifactConstraint) {
426
+ classification = { kind: "retryable", code: "red-artifact-constraint" };
427
+ } else if (agentsRequested.length > 0) {
428
+ classification = { kind: "paused", code: "agents-context-needed" };
429
+ } else {
430
+ classification = {
431
+ kind: request.code === "approved-context-needed" ? "retryable" : "paused",
432
+ code:
433
+ request.code === "task-split-needed"
434
+ ? "task-split-needed"
435
+ : "approved-context-needed",
436
+ };
437
+ }
215
438
  return {
216
- kind: approvalNeeded ? "approval-needed" : "paused",
217
- code: inBoundary ? request.code : "boundary-review-needed",
439
+ ...classification,
218
440
  contextRequest: {
219
441
  code: request.code,
220
442
  refs,
@@ -256,10 +478,16 @@ export function createCandidateArtifactTool(
256
478
  executionMode: "sequential",
257
479
  parameters,
258
480
  async execute(_toolCallId, params) {
481
+ attempts++;
259
482
  if (result !== undefined) {
483
+ schema = "invalid";
484
+ failure = {
485
+ kind: "artifact",
486
+ code: "invalid-structural-result",
487
+ stage: "structural-submit",
488
+ };
260
489
  throw new Error("candidate terminal result already submitted");
261
490
  }
262
- attempts++;
263
491
  if (attempts > 2) {
264
492
  schema = "invalid";
265
493
  failure = {
@@ -391,18 +619,17 @@ export function createCandidateArtifactTool(
391
619
  terminate: true,
392
620
  };
393
621
  }
394
- const refs = value.refs as string[];
395
- if (
396
- refs.some((relative) => !isValidRelativePath(relative)) ||
397
- new Set(refs).size !== refs.length
398
- ) {
622
+ let refs: CandidateContextRef[];
623
+ try {
624
+ refs = normalizeCandidateContextRefs(value.refs);
625
+ } catch (error) {
399
626
  schema = "invalid";
400
627
  failure = {
401
628
  kind: "artifact",
402
629
  code: "invalid-structural-result",
403
630
  stage: "structural-submit",
404
631
  };
405
- throw new Error("context request refs are invalid");
632
+ throw error;
406
633
  }
407
634
  result = {
408
635
  kind: "context-request",
@@ -411,7 +638,7 @@ export function createCandidateArtifactTool(
411
638
  CandidateArtifactSubmissionResult,
412
639
  { kind: "context-request" }
413
640
  >["code"],
414
- refs: [...refs],
641
+ refs,
415
642
  };
416
643
  return {
417
644
  content: [
@@ -434,6 +661,170 @@ export function createCandidateArtifactTool(
434
661
  };
435
662
  }
436
663
 
664
+ export function createStructuredPatchTool(input: {
665
+ requestId: string;
666
+ taskId?: string;
667
+ role: string;
668
+ phase: string;
669
+ workspaceRoot: string;
670
+ writePaths: string[];
671
+ deletePaths: string[];
672
+ }) {
673
+ let submitted: DiffResult | CandidateContextRequest | undefined;
674
+ let failure: CandidateFailure | undefined;
675
+ let attempts = 0;
676
+ let schema: SubmitSchemaState = "not-submitted";
677
+ let identity: IdentityOutcome = {
678
+ request: true,
679
+ role: true,
680
+ task: true,
681
+ phase: true,
682
+ };
683
+ const candidateId = input.requestId;
684
+ const tool = defineTool({
685
+ name: "abel_submit_result",
686
+ label: "Submit Abel Structured Patch",
687
+ description:
688
+ "Submit one complete structured patch. Unified-diff headers and hunks are generated by the trusted control plane.",
689
+ executionMode: "sequential",
690
+ parameters: candidateArtifactSchema(candidateId),
691
+ async execute(_toolCallId, params) {
692
+ attempts += 1;
693
+ if (submitted !== undefined) {
694
+ schema = "invalid";
695
+ failure = {
696
+ kind: "artifact",
697
+ code: "invalid-structural-result",
698
+ stage: "structural-submit",
699
+ };
700
+ throw new Error("structured patch terminal result already submitted");
701
+ }
702
+ if (attempts > 2) {
703
+ schema = "invalid";
704
+ failure = {
705
+ kind: "artifact",
706
+ code: "invalid-structural-result",
707
+ stage: "structural-submit",
708
+ };
709
+ throw new Error("structural submission correction limit exceeded");
710
+ }
711
+ identity = {
712
+ request: true,
713
+ role: true,
714
+ task: true,
715
+ phase: true,
716
+ };
717
+ failure = undefined;
718
+ const value = params as unknown as Record<string, unknown>;
719
+ if (value.candidateId !== candidateId) {
720
+ identity.request = false;
721
+ schema = "invalid";
722
+ failure = {
723
+ kind: "artifact",
724
+ code: "structural-identity-mismatch",
725
+ stage: "structural-submit",
726
+ };
727
+ throw new Error("diagnosis candidate identity mismatch");
728
+ }
729
+ if (value.kind === "context-request") {
730
+ let refs: CandidateContextRef[];
731
+ try {
732
+ refs = normalizeCandidateContextRefs(value.refs);
733
+ } catch (error) {
734
+ schema = "invalid";
735
+ failure = {
736
+ kind: "artifact",
737
+ code: "invalid-structural-result",
738
+ stage: "structural-submit",
739
+ };
740
+ throw error;
741
+ }
742
+ submitted = {
743
+ kind: "context-request",
744
+ candidateId,
745
+ code: value.code as CandidateContextRequest["code"],
746
+ refs,
747
+ };
748
+ schema = "valid";
749
+ return {
750
+ content: [
751
+ { type: "text" as const, text: "Context request accepted." },
752
+ ],
753
+ details: { accepted: true },
754
+ terminate: true,
755
+ };
756
+ }
757
+ if (value.kind !== "candidate-patch") {
758
+ schema = "invalid";
759
+ failure = {
760
+ kind: "artifact",
761
+ code: "invalid-structural-result",
762
+ stage: "structural-submit",
763
+ };
764
+ throw new Error("diagnosis result must be a patch or context request");
765
+ }
766
+ let compiled: ReturnType<typeof compileCandidatePatch>;
767
+ try {
768
+ compiled = compileCandidatePatch({
769
+ root: input.workspaceRoot,
770
+ operations: value.operations as CandidatePatchOperation[],
771
+ writePaths: input.writePaths,
772
+ deletePaths: input.deletePaths,
773
+ maxBytes: LIMITS.maxCompleteResultBytes,
774
+ });
775
+ } catch (error) {
776
+ schema = "invalid";
777
+ failure = {
778
+ kind: "artifact",
779
+ code: "invalid-diff",
780
+ stage: "candidate-diff",
781
+ };
782
+ throw error;
783
+ }
784
+ const result: DiffResult = {
785
+ id: input.requestId,
786
+ role: input.role,
787
+ kind: "diff",
788
+ taskId: input.taskId ?? input.requestId,
789
+ phase: input.phase,
790
+ summary:
791
+ "Trusted control plane generated this diff from structured operations.",
792
+ diff: compiled,
793
+ expectedVerification: "parent-owned verification contract",
794
+ risks: [],
795
+ contractCompliant: true,
796
+ };
797
+ const validation = validateDiffResult(result);
798
+ if (!validation.ok) {
799
+ schema = "invalid";
800
+ failure = validation.failure ?? {
801
+ kind: "artifact",
802
+ code: "invalid-structural-result",
803
+ stage: "structural-submit",
804
+ };
805
+ throw new Error(validation.reason ?? "structured patch is invalid");
806
+ }
807
+ submitted = result;
808
+ schema = "valid";
809
+ return {
810
+ content: [{ type: "text" as const, text: "Abel patch accepted." }],
811
+ details: { accepted: true },
812
+ terminate: true,
813
+ };
814
+ },
815
+ });
816
+ return {
817
+ tool,
818
+ getResult: () =>
819
+ submitted === undefined ? undefined : structuredClone(submitted),
820
+ getAttempts: () => attempts,
821
+ getSchema: () => schema,
822
+ getIdentity: () => ({ ...identity }),
823
+ getFailure: () =>
824
+ failure === undefined ? undefined : structuredClone(failure),
825
+ };
826
+ }
827
+
437
828
  export type FinalCategory = SubmitFinalCategory;
438
829
 
439
830
  export interface IdentityOutcome {
@@ -482,6 +873,15 @@ export function createSubmitTool(input: {
482
873
  : diffSchema,
483
874
  async execute(_toolCallId, params) {
484
875
  attempts++;
876
+ if (submitted !== undefined) {
877
+ schema = "invalid";
878
+ failure = {
879
+ kind: "artifact",
880
+ code: "invalid-structural-result",
881
+ stage: "structural-submit",
882
+ };
883
+ throw new Error("duplicate structural submission");
884
+ }
485
885
  if (attempts > 2) {
486
886
  schema = "invalid";
487
887
  failure = {
@@ -548,14 +948,6 @@ export function createSubmitTool(input: {
548
948
  validation.reason ?? "submitted result identity does not match",
549
949
  );
550
950
  }
551
- if (submitted) {
552
- failure = {
553
- kind: "artifact",
554
- code: "invalid-structural-result",
555
- stage: "structural-submit",
556
- };
557
- throw new Error("duplicate structural submission");
558
- }
559
951
  submitted = structuredClone(value) as unknown as
560
952
  | EvidenceResult
561
953
  | DiffResult;
@@ -230,7 +230,7 @@ function hashEvent(
230
230
  eventJson: string,
231
231
  ): string {
232
232
  return createHash("sha256")
233
- .update("cadence-task-ledger-event-v2\0")
233
+ .update("cadence-task-ledger-event\0")
234
234
  .update(priorHash)
235
235
  .update("\0")
236
236
  .update(String(ordinal))
@@ -1354,7 +1354,6 @@ export class TaskLedger {
1354
1354
  return { ordinal: row.ordinal, ...fact };
1355
1355
  });
1356
1356
  const projection = {
1357
- version: 2,
1358
1357
  runId: input.runId,
1359
1358
  taskId: input.taskId,
1360
1359
  deliveryRevision: task.delivery_revision,