@dieulc/workflow 0.9.2 → 0.10.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 +69 -7
- package/index.ts +827 -155
- package/package.json +9 -6
- package/provider-retarget.ts +258 -0
- package/review-handoff.ts +125 -0
- package/review-recovery.ts +65 -0
- package/review-runtime.ts +318 -48
- package/review-tools.ts +303 -6
- package/review.ts +242 -2
package/review-tools.ts
CHANGED
|
@@ -373,9 +373,28 @@ export interface ReviewSubmitDeps {
|
|
|
373
373
|
submit: (
|
|
374
374
|
payload: ReviewSubmitPayload,
|
|
375
375
|
) => Promise<{ ok: boolean; path?: string; error?: string }>;
|
|
376
|
+
/** Chunked submission is optional so older callers/tests keep working. */
|
|
377
|
+
beginPlan?: (totalChunks?: number) => {
|
|
378
|
+
ok: boolean;
|
|
379
|
+
error?: string;
|
|
380
|
+
maxChunkChars?: number;
|
|
381
|
+
};
|
|
382
|
+
appendPlanChunk?: (index: number, content: string) => {
|
|
383
|
+
ok: boolean;
|
|
384
|
+
error?: string;
|
|
385
|
+
received?: number;
|
|
386
|
+
nextIndex?: number;
|
|
387
|
+
totalChars?: number;
|
|
388
|
+
};
|
|
389
|
+
takeAssembledPlan?: () => string | undefined;
|
|
390
|
+
resetPlan?: () => void;
|
|
391
|
+
/** Max characters accepted per chunk (default 6000). */
|
|
392
|
+
maxChunkChars?: number;
|
|
376
393
|
log: (message: string, level?: "info" | "warning" | "error") => void;
|
|
377
394
|
}
|
|
378
395
|
|
|
396
|
+
const DEFAULT_MAX_CHUNK_CHARS = 6_000;
|
|
397
|
+
|
|
379
398
|
const VERDICT_SCHEMA = Type.Union([
|
|
380
399
|
Type.Literal("approve"),
|
|
381
400
|
Type.Literal("revise"),
|
|
@@ -413,6 +432,196 @@ const FINDING_SCHEMA = Type.Object({
|
|
|
413
432
|
]),
|
|
414
433
|
});
|
|
415
434
|
|
|
435
|
+
/**
|
|
436
|
+
* `review_plan_begin` — start a chunked rewrite submission.
|
|
437
|
+
*
|
|
438
|
+
* Long plan rewrites can outlive a gateway's stream duration. This resets the
|
|
439
|
+
* assembler; the reviewer then sends ordered chunks and finalises through
|
|
440
|
+
* `review_submit_plan` (with no `planMarkdown`). The single-shot path remains.
|
|
441
|
+
*/
|
|
442
|
+
export function createReviewPlanBeginTool(deps: ReviewSubmitDeps): {
|
|
443
|
+
name: string;
|
|
444
|
+
label: string;
|
|
445
|
+
description: string;
|
|
446
|
+
parameters: TSchema;
|
|
447
|
+
execute: (...args: unknown[]) => Promise<ReviewToolTextResult>;
|
|
448
|
+
} {
|
|
449
|
+
return {
|
|
450
|
+
name: "review_plan_begin",
|
|
451
|
+
label: "Begin Chunked Plan",
|
|
452
|
+
description:
|
|
453
|
+
"Start submitting the revised plan in ordered chunks. Use this when the plan is too long for one response, then call review_plan_chunk for each part and review_submit_plan (without planMarkdown) to finalise.",
|
|
454
|
+
parameters: Type.Object({
|
|
455
|
+
totalChunks: Type.Optional(
|
|
456
|
+
Type.Number({
|
|
457
|
+
description:
|
|
458
|
+
"How many chunks you intend to send. Optional; helps the harness report progress.",
|
|
459
|
+
}),
|
|
460
|
+
),
|
|
461
|
+
}),
|
|
462
|
+
async execute(...args: unknown[]): Promise<ReviewToolTextResult> {
|
|
463
|
+
const [, rawParams] = args as unknown as [
|
|
464
|
+
string,
|
|
465
|
+
{ totalChunks?: unknown },
|
|
466
|
+
];
|
|
467
|
+
if (!deps.beginPlan) {
|
|
468
|
+
return {
|
|
469
|
+
content: [
|
|
470
|
+
{
|
|
471
|
+
type: "text",
|
|
472
|
+
text: "review_plan_begin: chunked submission is unavailable in this round; use review_submit_plan with the complete planMarkdown instead.",
|
|
473
|
+
},
|
|
474
|
+
],
|
|
475
|
+
isError: true,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
const totalChunks =
|
|
479
|
+
typeof rawParams?.totalChunks === "number" &&
|
|
480
|
+
Number.isInteger(rawParams.totalChunks) &&
|
|
481
|
+
rawParams.totalChunks > 0
|
|
482
|
+
? rawParams.totalChunks
|
|
483
|
+
: undefined;
|
|
484
|
+
const result = deps.beginPlan(totalChunks);
|
|
485
|
+
if (!result.ok) {
|
|
486
|
+
return {
|
|
487
|
+
content: [
|
|
488
|
+
{
|
|
489
|
+
type: "text",
|
|
490
|
+
text: `review_plan_begin: ${result.error ?? "could not start chunked submission"}`,
|
|
491
|
+
},
|
|
492
|
+
],
|
|
493
|
+
isError: true,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
const maxChunkChars =
|
|
497
|
+
result.maxChunkChars ?? deps.maxChunkChars ?? DEFAULT_MAX_CHUNK_CHARS;
|
|
498
|
+
return {
|
|
499
|
+
content: [
|
|
500
|
+
{
|
|
501
|
+
type: "text",
|
|
502
|
+
text: [
|
|
503
|
+
"review_plan_begin: ready.",
|
|
504
|
+
`Send the plan in order with review_plan_chunk (max ${maxChunkChars} characters per chunk).`,
|
|
505
|
+
"After the last chunk, call review_plan_finalize (or review_submit_plan without planMarkdown).",
|
|
506
|
+
].join("\n"),
|
|
507
|
+
},
|
|
508
|
+
],
|
|
509
|
+
details: { totalChunks, maxChunkChars },
|
|
510
|
+
};
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** `review_plan_chunk` — append one ordered part of the rewrite. */
|
|
516
|
+
export function createReviewPlanChunkTool(deps: ReviewSubmitDeps): {
|
|
517
|
+
name: string;
|
|
518
|
+
label: string;
|
|
519
|
+
description: string;
|
|
520
|
+
parameters: TSchema;
|
|
521
|
+
execute: (...args: unknown[]) => Promise<ReviewToolTextResult>;
|
|
522
|
+
} {
|
|
523
|
+
return {
|
|
524
|
+
name: "review_plan_chunk",
|
|
525
|
+
label: "Append Plan Chunk",
|
|
526
|
+
description:
|
|
527
|
+
"Append one ordered chunk of the revised plan. Start with review_plan_begin, send index 1, 2, … without skipping or repeating, then finalise with review_submit_plan (no planMarkdown).",
|
|
528
|
+
parameters: Type.Object({
|
|
529
|
+
index: Type.Number({
|
|
530
|
+
description: "1-based chunk index. Must be the next expected index.",
|
|
531
|
+
}),
|
|
532
|
+
content: Type.String({
|
|
533
|
+
description:
|
|
534
|
+
"The chunk's markdown. Do not repeat earlier chunks or add a Review changes section.",
|
|
535
|
+
}),
|
|
536
|
+
}),
|
|
537
|
+
async execute(...args: unknown[]): Promise<ReviewToolTextResult> {
|
|
538
|
+
const [, rawParams] = args as unknown as [
|
|
539
|
+
string,
|
|
540
|
+
{ index?: unknown; content?: unknown },
|
|
541
|
+
];
|
|
542
|
+
const index =
|
|
543
|
+
typeof rawParams?.index === "number" ? rawParams.index : NaN;
|
|
544
|
+
if (!Number.isInteger(index) || index < 1) {
|
|
545
|
+
return {
|
|
546
|
+
content: [
|
|
547
|
+
{
|
|
548
|
+
type: "text",
|
|
549
|
+
text: "review_plan_chunk: index must be a positive integer.",
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
isError: true,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
const content =
|
|
556
|
+
typeof rawParams?.content === "string" ? rawParams.content : "";
|
|
557
|
+
if (!content) {
|
|
558
|
+
return {
|
|
559
|
+
content: [
|
|
560
|
+
{
|
|
561
|
+
type: "text",
|
|
562
|
+
text: "review_plan_chunk: content is empty.",
|
|
563
|
+
},
|
|
564
|
+
],
|
|
565
|
+
isError: true,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
const maxChunkChars = deps.maxChunkChars ?? DEFAULT_MAX_CHUNK_CHARS;
|
|
569
|
+
if (content.length > maxChunkChars) {
|
|
570
|
+
return {
|
|
571
|
+
content: [
|
|
572
|
+
{
|
|
573
|
+
type: "text",
|
|
574
|
+
text: `review_plan_chunk: chunk ${index} is ${content.length} characters; the limit is ${maxChunkChars}. Split it and resend.`,
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
isError: true,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
if (!deps.appendPlanChunk) {
|
|
581
|
+
return {
|
|
582
|
+
content: [
|
|
583
|
+
{
|
|
584
|
+
type: "text",
|
|
585
|
+
text: "review_plan_chunk: chunked submission is unavailable in this round; use review_submit_plan with the complete planMarkdown instead.",
|
|
586
|
+
},
|
|
587
|
+
],
|
|
588
|
+
isError: true,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
const result = deps.appendPlanChunk(index, content);
|
|
592
|
+
if (!result.ok) {
|
|
593
|
+
return {
|
|
594
|
+
content: [
|
|
595
|
+
{
|
|
596
|
+
type: "text",
|
|
597
|
+
text: `review_plan_chunk: ${result.error ?? "chunk rejected"}`,
|
|
598
|
+
},
|
|
599
|
+
],
|
|
600
|
+
isError: true,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
const nextIndex = result.nextIndex ?? index + 1;
|
|
604
|
+
return {
|
|
605
|
+
content: [
|
|
606
|
+
{
|
|
607
|
+
type: "text",
|
|
608
|
+
text: [
|
|
609
|
+
`review_plan_chunk: accepted chunk ${index}.`,
|
|
610
|
+
`received ${result.received ?? index} chunk(s), ${result.totalChars ?? content.length} characters so far.`,
|
|
611
|
+
`Next expected index: ${nextIndex}. When done, call review_submit_plan without planMarkdown.`,
|
|
612
|
+
].join("\n"),
|
|
613
|
+
},
|
|
614
|
+
],
|
|
615
|
+
details: {
|
|
616
|
+
index,
|
|
617
|
+
nextIndex,
|
|
618
|
+
totalChars: result.totalChars ?? content.length,
|
|
619
|
+
},
|
|
620
|
+
};
|
|
621
|
+
},
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
416
625
|
/**
|
|
417
626
|
* `review_submit_plan` — the ONLY write path for Review Mode.
|
|
418
627
|
*
|
|
@@ -438,12 +647,14 @@ export function createReviewSubmitPlanTool(deps: ReviewSubmitDeps): {
|
|
|
438
647
|
name: "review_submit_plan",
|
|
439
648
|
label: "Submit Reviewed Plan",
|
|
440
649
|
description:
|
|
441
|
-
"Submit the FINAL revised plan. This is the only way your work reaches the user, and the only file you can write. Pass the complete plan markdown (not a diff or a summary) plus your findings and verdict. The plan body must keep a markdown heading and stay close to the original length; the harness rejects stubs. A `## Review changes` section is appended automatically from your findings, so do not write it yourself. You may call this more than once — the last successful submission wins.",
|
|
650
|
+
"Submit the FINAL revised plan. This is the only way your work reaches the user, and the only file you can write. Pass the complete plan markdown (not a diff or a summary) plus your findings and verdict. After review_plan_begin/review_plan_chunk, omit planMarkdown and the harness uses the assembled chunks. The plan body must keep a markdown heading and stay close to the original length; the harness rejects stubs. A `## Review changes` section is appended automatically from your findings, so do not write it yourself. You may call this more than once — the last successful submission wins.",
|
|
442
651
|
parameters: Type.Object({
|
|
443
|
-
planMarkdown: Type.
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
652
|
+
planMarkdown: Type.Optional(
|
|
653
|
+
Type.String({
|
|
654
|
+
description:
|
|
655
|
+
"The complete revised plan in markdown, preserving the original structure and heading style. Omit it after review_plan_begin/review_plan_chunk. Do NOT include a 'Review changes' section.",
|
|
656
|
+
}),
|
|
657
|
+
),
|
|
447
658
|
findings: Type.Array(FINDING_SCHEMA, {
|
|
448
659
|
description:
|
|
449
660
|
"Every issue you found. Use an empty array if the plan is sound and you are submitting it unchanged.",
|
|
@@ -466,10 +677,24 @@ export function createReviewSubmitPlanTool(deps: ReviewSubmitDeps): {
|
|
|
466
677
|
},
|
|
467
678
|
];
|
|
468
679
|
|
|
469
|
-
const
|
|
680
|
+
const explicitPlan =
|
|
470
681
|
typeof rawParams?.planMarkdown === "string"
|
|
471
682
|
? rawParams.planMarkdown
|
|
472
683
|
: "";
|
|
684
|
+
const planBody = explicitPlan.trim()
|
|
685
|
+
? explicitPlan
|
|
686
|
+
: deps.takeAssembledPlan?.() ?? "";
|
|
687
|
+
if (!planBody.trim()) {
|
|
688
|
+
return {
|
|
689
|
+
content: [
|
|
690
|
+
{
|
|
691
|
+
type: "text",
|
|
692
|
+
text: "review_submit_plan: no plan was provided. Pass planMarkdown, or use review_plan_begin + review_plan_chunk first.",
|
|
693
|
+
},
|
|
694
|
+
],
|
|
695
|
+
isError: true,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
473
698
|
const verdict: ReviewVerdict =
|
|
474
699
|
rawParams?.verdict === "approve" ||
|
|
475
700
|
rawParams?.verdict === "revise" ||
|
|
@@ -563,6 +788,11 @@ export function createReviewSubmitPlanTool(deps: ReviewSubmitDeps): {
|
|
|
563
788
|
};
|
|
564
789
|
}
|
|
565
790
|
|
|
791
|
+
try {
|
|
792
|
+
deps.resetPlan?.();
|
|
793
|
+
} catch {
|
|
794
|
+
/* the written plan is authoritative; reset is best-effort */
|
|
795
|
+
}
|
|
566
796
|
return {
|
|
567
797
|
content: [
|
|
568
798
|
{
|
|
@@ -590,6 +820,70 @@ export function createReviewSubmitPlanTool(deps: ReviewSubmitDeps): {
|
|
|
590
820
|
};
|
|
591
821
|
}
|
|
592
822
|
|
|
823
|
+
// ── Step 17: review_plan_finalize ────────────────────────────────────
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* `review_plan_finalize` — finalise a chunked rewrite.
|
|
827
|
+
*
|
|
828
|
+
* The chunks were accepted by `review_plan_chunk`; this is the step that turns
|
|
829
|
+
* them into the plan file. It delegates to the SAME `review_submit_plan`
|
|
830
|
+
* funnel, so `writePlan` stays the only writer and `notifySubmitted` fires once.
|
|
831
|
+
*/
|
|
832
|
+
export function createReviewPlanFinalizeTool(deps: ReviewSubmitDeps): {
|
|
833
|
+
name: string;
|
|
834
|
+
label: string;
|
|
835
|
+
description: string;
|
|
836
|
+
parameters: TSchema;
|
|
837
|
+
execute: (...args: unknown[]) => Promise<ReviewToolTextResult>;
|
|
838
|
+
} {
|
|
839
|
+
const submit = createReviewSubmitPlanTool(deps);
|
|
840
|
+
return {
|
|
841
|
+
name: "review_plan_finalize",
|
|
842
|
+
label: "Finalize Chunked Plan",
|
|
843
|
+
description:
|
|
844
|
+
"Finalise the plan you sent with review_plan_begin + review_plan_chunk. The assembled chunks become the plan body; the findings and verdict are recorded exactly as review_submit_plan would. Refuses an incomplete assembly — send the missing chunks first.",
|
|
845
|
+
parameters: Type.Object({
|
|
846
|
+
findings: Type.Array(FINDING_SCHEMA, {
|
|
847
|
+
description:
|
|
848
|
+
"Every issue you found. Use an empty array if the plan is sound and you are submitting it unchanged.",
|
|
849
|
+
}),
|
|
850
|
+
verdict: VERDICT_SCHEMA,
|
|
851
|
+
summary: Type.String({
|
|
852
|
+
description: "One sentence describing what you changed.",
|
|
853
|
+
}),
|
|
854
|
+
}),
|
|
855
|
+
async execute(...args: unknown[]): Promise<ReviewToolTextResult> {
|
|
856
|
+
const [toolCallId, rawParams, ...rest] = args as [
|
|
857
|
+
string,
|
|
858
|
+
Record<string, unknown>,
|
|
859
|
+
...unknown[],
|
|
860
|
+
];
|
|
861
|
+
let assembled: string | undefined;
|
|
862
|
+
try {
|
|
863
|
+
assembled = deps.takeAssembledPlan?.();
|
|
864
|
+
} catch {
|
|
865
|
+
assembled = undefined;
|
|
866
|
+
}
|
|
867
|
+
if (!assembled || !assembled.trim()) {
|
|
868
|
+
return {
|
|
869
|
+
content: [
|
|
870
|
+
{
|
|
871
|
+
type: "text",
|
|
872
|
+
text: "review_plan_finalize: no complete plan to finalise. Call review_plan_begin, send every chunk with review_plan_chunk (watch the expected next index), then finalise.",
|
|
873
|
+
},
|
|
874
|
+
],
|
|
875
|
+
isError: true,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
return submit.execute(
|
|
879
|
+
toolCallId,
|
|
880
|
+
{ ...(rawParams ?? {}) },
|
|
881
|
+
...rest,
|
|
882
|
+
);
|
|
883
|
+
},
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
593
887
|
// ── Step 15: review_pass_done ────────────────────────────────────────
|
|
594
888
|
|
|
595
889
|
/**
|
|
@@ -739,6 +1033,9 @@ export function createReviewTools(
|
|
|
739
1033
|
}
|
|
740
1034
|
|
|
741
1035
|
tools.push(createReviewExploreTool(deps.explore));
|
|
1036
|
+
tools.push(createReviewPlanBeginTool(deps.submit));
|
|
1037
|
+
tools.push(createReviewPlanChunkTool(deps.submit));
|
|
1038
|
+
tools.push(createReviewPlanFinalizeTool(deps.submit));
|
|
742
1039
|
tools.push(createReviewSubmitPlanTool(deps.submit));
|
|
743
1040
|
tools.push(createReviewPassDoneTool(deps.passDone));
|
|
744
1041
|
|