@try-works/dsh-recursive-mode 0.2.3 → 0.3.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/lib/client/doc-viewer.d.ts +40 -0
- package/lib/client/host-api.d.ts +13 -0
- package/lib/client/index.d.ts +1 -0
- package/lib/client.js +813 -58
- package/lib/delegation.d.ts +168 -0
- package/lib/enforcement.d.ts +8 -0
- package/lib/goals-projection.d.ts +92 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +915 -68
- package/lib/live-route.d.ts +1 -1
- package/lib/recursive_audit_team.tool.d.ts +2 -0
- package/lib/runtime.d.ts +112 -3
- package/lib/teams-loop.d.ts +160 -0
- package/package.json +1 -1
- package/scripts/test-recursive-mode-smoke.ts +45 -19
- package/src/client/doc-viewer.tsx +306 -0
- package/src/client/host-api.ts +28 -0
- package/src/client/index.ts +1 -0
- package/src/client/inspector.tsx +13 -2
- package/src/client/styles.ts +310 -0
- package/src/delegation.ts +337 -1
- package/src/enforcement.ts +16 -1
- package/src/goals-projection.ts +149 -0
- package/src/index.ts +41 -11
- package/src/live-route.ts +69 -2
- package/src/recursive_audit_team.tool.ts +141 -0
- package/src/runtime.ts +134 -6
- package/src/teams-loop.ts +259 -0
package/src/client/styles.ts
CHANGED
|
@@ -532,6 +532,316 @@ const BOARD_CSS = `
|
|
|
532
532
|
color: var(--board-muted-fg);
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
+
/* ===== Per-phase doc viewer (0.2.4, inside the inspector detail body) ===== */
|
|
536
|
+
|
|
537
|
+
/* View phase ghost button on each present phase row. */
|
|
538
|
+
.rec-phase-view {
|
|
539
|
+
flex: none;
|
|
540
|
+
padding: 3px 10px;
|
|
541
|
+
font-size: var(--board-text-xs);
|
|
542
|
+
color: var(--board-info);
|
|
543
|
+
background: transparent;
|
|
544
|
+
border: 1px solid var(--board-border);
|
|
545
|
+
border-radius: 999px;
|
|
546
|
+
cursor: pointer;
|
|
547
|
+
white-space: nowrap;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.rec-phase-view:hover {
|
|
551
|
+
background: var(--board-accent);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/* Viewer shell: fills the inspector detail body (the panel owns data-theme). */
|
|
555
|
+
.rec-doc {
|
|
556
|
+
display: flex;
|
|
557
|
+
flex-direction: column;
|
|
558
|
+
min-height: 0;
|
|
559
|
+
gap: var(--board-space-12);
|
|
560
|
+
color: var(--board-fg);
|
|
561
|
+
font-family: var(--board-font-sans);
|
|
562
|
+
background: var(--board-bg);
|
|
563
|
+
border: 1px solid var(--board-border);
|
|
564
|
+
border-radius: var(--board-radius-xl);
|
|
565
|
+
overflow: hidden;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.rec-doc-header {
|
|
569
|
+
display: flex;
|
|
570
|
+
align-items: center;
|
|
571
|
+
gap: var(--board-space-12);
|
|
572
|
+
padding: var(--board-space-12) var(--board-space-16);
|
|
573
|
+
border-bottom: 1px solid var(--board-border);
|
|
574
|
+
flex: none;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.rec-doc-heading {
|
|
578
|
+
display: flex;
|
|
579
|
+
align-items: baseline;
|
|
580
|
+
gap: var(--board-space-12);
|
|
581
|
+
flex: 1;
|
|
582
|
+
min-width: 0;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.rec-doc-badge {
|
|
586
|
+
flex: none;
|
|
587
|
+
padding: 2px 10px;
|
|
588
|
+
font-size: 11px;
|
|
589
|
+
font-weight: var(--board-fw-semibold);
|
|
590
|
+
letter-spacing: 0.05em;
|
|
591
|
+
text-transform: uppercase;
|
|
592
|
+
border-radius: 999px;
|
|
593
|
+
background: var(--board-accent);
|
|
594
|
+
color: var(--board-muted-fg);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.rec-doc-title {
|
|
598
|
+
margin: 0;
|
|
599
|
+
font-size: var(--board-text-sm);
|
|
600
|
+
font-weight: var(--board-fw-semibold);
|
|
601
|
+
letter-spacing: var(--board-tracking-tight);
|
|
602
|
+
overflow: hidden;
|
|
603
|
+
text-overflow: ellipsis;
|
|
604
|
+
white-space: nowrap;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
.rec-doc-run {
|
|
608
|
+
flex: none;
|
|
609
|
+
font-family: var(--board-font-mono);
|
|
610
|
+
font-size: var(--board-text-xs);
|
|
611
|
+
color: var(--board-muted-fg);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.rec-doc-btn {
|
|
615
|
+
flex: none;
|
|
616
|
+
padding: 5px 12px;
|
|
617
|
+
font-size: var(--board-text-xs);
|
|
618
|
+
color: var(--board-fg);
|
|
619
|
+
background: transparent;
|
|
620
|
+
border: 1px solid var(--board-border);
|
|
621
|
+
border-radius: 999px;
|
|
622
|
+
cursor: pointer;
|
|
623
|
+
white-space: nowrap;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.rec-doc-btn:hover {
|
|
627
|
+
background: var(--board-accent);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/* Search bar (/ to open). */
|
|
631
|
+
.rec-doc-search {
|
|
632
|
+
display: flex;
|
|
633
|
+
align-items: center;
|
|
634
|
+
gap: var(--board-space-12);
|
|
635
|
+
padding: var(--board-space-8) var(--board-space-16);
|
|
636
|
+
border-bottom: 1px solid var(--board-border);
|
|
637
|
+
flex: none;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.rec-doc-search-input {
|
|
641
|
+
flex: 1 1 auto;
|
|
642
|
+
background: var(--board-muted);
|
|
643
|
+
color: var(--board-fg);
|
|
644
|
+
border: 1px solid var(--board-border);
|
|
645
|
+
border-radius: var(--board-radius-md);
|
|
646
|
+
padding: 5px 10px;
|
|
647
|
+
font-size: var(--board-text-xs);
|
|
648
|
+
font-family: inherit;
|
|
649
|
+
min-width: 0;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.rec-doc-search-input::placeholder {
|
|
653
|
+
color: var(--board-muted-fg);
|
|
654
|
+
opacity: 1;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.rec-doc-search-count {
|
|
658
|
+
font-size: var(--board-text-xs);
|
|
659
|
+
color: var(--board-muted-fg);
|
|
660
|
+
white-space: nowrap;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/* Body: scrollable line list. */
|
|
664
|
+
.rec-doc-body {
|
|
665
|
+
flex: 1 1 auto;
|
|
666
|
+
min-height: 0;
|
|
667
|
+
overflow-y: auto;
|
|
668
|
+
padding: var(--board-space-12) var(--board-space-16) var(--board-space-16);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
.rec-doc-line {
|
|
672
|
+
font-size: var(--board-text-sm);
|
|
673
|
+
line-height: 1.7;
|
|
674
|
+
padding: 0 6px;
|
|
675
|
+
border-left: 2px solid transparent;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.rec-doc-line-current {
|
|
679
|
+
background: var(--board-muted);
|
|
680
|
+
border-left-color: var(--board-info);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
.rec-doc-blank {
|
|
684
|
+
height: 10px;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.rec-doc-plain {
|
|
688
|
+
white-space: pre-wrap;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
.rec-doc-h1 {
|
|
692
|
+
font-size: 22px;
|
|
693
|
+
font-weight: 700;
|
|
694
|
+
margin: 8px 0 6px;
|
|
695
|
+
line-height: 1.3;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
.rec-doc-h2 {
|
|
699
|
+
font-size: 18px;
|
|
700
|
+
font-weight: 600;
|
|
701
|
+
margin: 12px 0 4px;
|
|
702
|
+
line-height: 1.3;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
.rec-doc-h3 {
|
|
706
|
+
font-size: 15px;
|
|
707
|
+
font-weight: 600;
|
|
708
|
+
margin: 10px 0 4px;
|
|
709
|
+
line-height: 1.3;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
.rec-doc-h4 {
|
|
713
|
+
font-size: 14px;
|
|
714
|
+
font-weight: 600;
|
|
715
|
+
margin: 8px 0 4px;
|
|
716
|
+
line-height: 1.3;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.rec-doc-li {
|
|
720
|
+
display: flex;
|
|
721
|
+
align-items: baseline;
|
|
722
|
+
gap: 8px;
|
|
723
|
+
font-size: var(--board-text-sm);
|
|
724
|
+
line-height: 1.65;
|
|
725
|
+
margin: 0 0 4px;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.rec-doc-bullet {
|
|
729
|
+
flex: none;
|
|
730
|
+
color: var(--board-muted-fg);
|
|
731
|
+
width: 14px;
|
|
732
|
+
text-align: center;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
.rec-doc-li-text {
|
|
736
|
+
flex: 1;
|
|
737
|
+
min-width: 0;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
.rec-doc-pre {
|
|
741
|
+
margin: 6px 0;
|
|
742
|
+
padding: var(--board-space-12);
|
|
743
|
+
background: var(--board-muted);
|
|
744
|
+
border: 1px solid var(--board-border);
|
|
745
|
+
border-radius: var(--board-radius-md);
|
|
746
|
+
overflow-x: auto;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
.rec-doc-code,
|
|
750
|
+
.rec-doc-inline-code {
|
|
751
|
+
font-family: var(--board-font-mono);
|
|
752
|
+
font-size: 12.5px;
|
|
753
|
+
letter-spacing: var(--board-tracking-mono);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.rec-doc-inline-code {
|
|
757
|
+
background: var(--board-muted);
|
|
758
|
+
border: 1px solid var(--board-border);
|
|
759
|
+
border-radius: 4px;
|
|
760
|
+
padding: 0 4px;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.rec-doc-inline-link {
|
|
764
|
+
color: var(--board-info);
|
|
765
|
+
text-decoration: underline;
|
|
766
|
+
cursor: pointer;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
.rec-doc-table {
|
|
770
|
+
width: 100%;
|
|
771
|
+
border-collapse: collapse;
|
|
772
|
+
margin: 6px 0;
|
|
773
|
+
font-size: var(--board-text-xs);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
.rec-doc-table th,
|
|
777
|
+
.rec-doc-table td {
|
|
778
|
+
border: 1px solid var(--board-border);
|
|
779
|
+
padding: 4px 8px;
|
|
780
|
+
text-align: left;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.rec-doc-table th {
|
|
784
|
+
background: var(--board-muted);
|
|
785
|
+
color: var(--board-muted-fg);
|
|
786
|
+
font-weight: var(--board-fw-semibold);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/* Footer: status + key hints. */
|
|
790
|
+
.rec-doc-footer {
|
|
791
|
+
flex: none;
|
|
792
|
+
display: flex;
|
|
793
|
+
align-items: center;
|
|
794
|
+
gap: var(--board-space-16);
|
|
795
|
+
padding: var(--board-space-8) var(--board-space-16);
|
|
796
|
+
border-top: 1px solid var(--board-border);
|
|
797
|
+
flex-wrap: wrap;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
.rec-doc-status {
|
|
801
|
+
font-size: var(--board-text-xs);
|
|
802
|
+
color: var(--board-muted-fg);
|
|
803
|
+
min-width: 120px;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.rec-doc-status-error {
|
|
807
|
+
color: var(--board-error);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
.rec-doc-status-ok {
|
|
811
|
+
color: var(--board-success);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
.rec-doc-hints {
|
|
815
|
+
display: flex;
|
|
816
|
+
align-items: center;
|
|
817
|
+
gap: 2px;
|
|
818
|
+
flex-wrap: wrap;
|
|
819
|
+
font-size: var(--board-text-xs);
|
|
820
|
+
color: var(--board-muted-fg);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
.rec-doc-hints kbd {
|
|
824
|
+
background: var(--board-muted);
|
|
825
|
+
border: 1px solid var(--board-border);
|
|
826
|
+
border-radius: 4px;
|
|
827
|
+
padding: 1px 6px;
|
|
828
|
+
font-family: inherit;
|
|
829
|
+
font-size: 11px;
|
|
830
|
+
font-weight: 600;
|
|
831
|
+
color: var(--board-fg);
|
|
832
|
+
margin: 0 3px 0 8px;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
.rec-doc-text {
|
|
836
|
+
margin: 0;
|
|
837
|
+
font-size: var(--board-text-sm);
|
|
838
|
+
color: var(--board-muted-fg);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
.rec-doc-error {
|
|
842
|
+
color: var(--board-error);
|
|
843
|
+
}
|
|
844
|
+
|
|
535
845
|
/* ===== Strip (session dock) — keeps the shell --dsw-* theme ===== */
|
|
536
846
|
.rec-badge {
|
|
537
847
|
flex: none;
|
package/src/delegation.ts
CHANGED
|
@@ -11,11 +11,53 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
|
11
11
|
import { join, resolve, sep } from 'node:path'
|
|
12
12
|
import { contentSha256 } from './review.ts'
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Opaque handle to the live direct-parent Agent. The live continuable service
|
|
16
|
+
* authorizes by EXACT live object identity — `ctx.agents.get(parent.id) ===
|
|
17
|
+
* parent` (authorizeLineage), `ancestry.has(parent)` (interrupt/drain), and a
|
|
18
|
+
* `WeakSet` of live ancestry — so this must be the real live `Agent`, never a
|
|
19
|
+
* structural `{ id }` copy. The seam only ever reads `id`/`session.header.cwd`
|
|
20
|
+
* for attribution, and never serializes or inspects the live object.
|
|
21
|
+
*/
|
|
22
|
+
export interface SubagentParentHandle {
|
|
23
|
+
readonly id?: string
|
|
24
|
+
readonly session?: { readonly header?: { readonly cwd?: string } }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Durable identity of one continuable child session (string-branded in the host). */
|
|
28
|
+
export type ContinuableChildId = string
|
|
29
|
+
|
|
30
|
+
/** Durable identity of one accepted inbox message (string-branded in the host). */
|
|
31
|
+
export type ContinuableMessageId = string
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Attribution for a model coordinator's follow-up to one of its children (the
|
|
35
|
+
* live `CoordinatorMessageSource` subset — see subagent/src/continuation.ts).
|
|
36
|
+
*/
|
|
37
|
+
export interface CoordinatorSourceLike {
|
|
38
|
+
readonly kind: 'coordinator'
|
|
39
|
+
readonly form: 'relay'
|
|
40
|
+
readonly senderSessionId: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Uniform outcome for the interrupt/drain kill-switch helpers. */
|
|
44
|
+
export interface ContinuableOpResult {
|
|
45
|
+
ok: boolean
|
|
46
|
+
reason?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
14
49
|
/** Minimal host-realm contract for ctx.subagents (the seam we call). */
|
|
15
50
|
export interface SubagentsRuntimeLike {
|
|
16
51
|
start(name: string, request: SubagentStartRequestLike): Promise<SubagentResultLike>
|
|
17
52
|
getProvider?(name: string): unknown
|
|
18
53
|
list?(): unknown
|
|
54
|
+
/** T4: continuable child lifecycle (startContinuable / followup / interrupt / drain). */
|
|
55
|
+
startContinuable?(spec: ContinuableStartSpecLike): Promise<ContinuableStartLike>
|
|
56
|
+
/** The parent MUST be the exact live Agent (object-identity authority), never a `{ id }` copy. */
|
|
57
|
+
followup?(parent: SubagentParentHandle, childId: ContinuableChildId, content: readonly { type: 'text'; text: string }[], options: SubagentFollowupOptionsLike): Promise<ContinuableMessageId>
|
|
58
|
+
interrupt?(targetSessionId: ContinuableChildId, authority: SubagentInterruptAuthorityLike): void
|
|
59
|
+
drainContinuableChildren?(parent: SubagentParentHandle, childIds: readonly ContinuableChildId[]): Promise<void>
|
|
60
|
+
drainContinuableDescendants?(parents: readonly SubagentParentHandle[]): Promise<void>
|
|
19
61
|
}
|
|
20
62
|
|
|
21
63
|
export interface SubagentStartRequestLike {
|
|
@@ -116,11 +158,305 @@ export async function delegate(input: {
|
|
|
116
158
|
}
|
|
117
159
|
}
|
|
118
160
|
|
|
161
|
+
/**
|
|
162
|
+
* T4: continuable-child delegation — ONE durable child receives the initial
|
|
163
|
+
* prompt (startContinuable), each REVISE is delivered as a followup to the SAME
|
|
164
|
+
* child (FIFO, working set retained), and the parent observes each round's
|
|
165
|
+
* settlement through the injected `awaitRoundResult` seam (in live usage the
|
|
166
|
+
* child's settlement lands in the parent's inbox — `reportFrom` is the
|
|
167
|
+
* CHILD-side API, so the parent-side loop collects via settlement, not by
|
|
168
|
+
* calling it). A hung reviewer is cancelled with `interruptContinuable`
|
|
169
|
+
* (keepInbox: the child's pending inbox survives). Falls back to one-shot
|
|
170
|
+
* `delegate` when the seam has no continuable methods.
|
|
171
|
+
*/
|
|
172
|
+
|
|
173
|
+
/** What the caller asks for when starting a continuable background child (structural subset). */
|
|
174
|
+
export interface ContinuableStartSpecLike {
|
|
175
|
+
/** The `ctx.subagents` provider whose continuable-creation capability establishes the child. */
|
|
176
|
+
readonly provider: string
|
|
177
|
+
/** The initial delegation's short `description`, persisted as the child's creation label. */
|
|
178
|
+
readonly label: string
|
|
179
|
+
/** Optional caller-reserved child identity. */
|
|
180
|
+
childId?: ContinuableChildId
|
|
181
|
+
/** The delegation request (prompt + parent + toolFilter + maxDepth; no label/signal/outputSchema). */
|
|
182
|
+
readonly request: Omit<SubagentStartRequestLike, 'label' | 'signal' | 'outputSchema'>
|
|
183
|
+
/** Caller cancellation, owning the operation only until inbox acceptance. */
|
|
184
|
+
readonly signal?: AbortSignalLike
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Minimal cancellation shape (a live AbortSignal satisfies it). */
|
|
188
|
+
export interface AbortSignalLike {
|
|
189
|
+
readonly throwIfAborted: () => void
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Identities returned once a continuable child accepted its initial prompt. */
|
|
193
|
+
export interface ContinuableStartLike {
|
|
194
|
+
/** The durable child session id, stable across activations. */
|
|
195
|
+
readonly childId: ContinuableChildId
|
|
196
|
+
/** The accepted initial prompt's inbox message id. */
|
|
197
|
+
readonly messageId: ContinuableMessageId
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Options for following up with one continuable child (structural subset). */
|
|
201
|
+
export interface SubagentFollowupOptionsLike {
|
|
202
|
+
/** Durable attribution retained on the delivered message. */
|
|
203
|
+
readonly source: CoordinatorSourceLike
|
|
204
|
+
/** Caller cancellation, owning the operation only until inbox acceptance. */
|
|
205
|
+
readonly signal?: AbortSignalLike
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Authority under which one interrupt request is admitted. */
|
|
209
|
+
export type SubagentInterruptAuthorityLike =
|
|
210
|
+
| { readonly kind: 'user'; readonly parentSessionId: string }
|
|
211
|
+
| { readonly kind: 'ancestor'; readonly agent: SubagentParentHandle }
|
|
212
|
+
|
|
213
|
+
/** One round of a continuable child: the delivered text plus the observed outcome. */
|
|
214
|
+
export interface ContinuableRoundLike {
|
|
215
|
+
/** The message text delivered as this round's user prompt. */
|
|
216
|
+
text: string
|
|
217
|
+
/** The child's observed outcome for this round. */
|
|
218
|
+
result?: SubagentResultLike
|
|
219
|
+
/** True when this round's verdict was REVISE (a repair followup followed). */
|
|
220
|
+
revise?: boolean
|
|
221
|
+
/** The repair instruction delivered in the followup (only when revise). */
|
|
222
|
+
repair?: string
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** The full T4 delegation outcome. */
|
|
226
|
+
export interface ContinuableDelegationLike {
|
|
227
|
+
ok: boolean
|
|
228
|
+
reason?: string
|
|
229
|
+
/** The durable child session id (stable across rounds). */
|
|
230
|
+
childId?: ContinuableChildId
|
|
231
|
+
/** Inbox message ids: [initial acceptance, ...followups]. */
|
|
232
|
+
messageIds?: ContinuableMessageId[]
|
|
233
|
+
rounds: ContinuableRoundLike[]
|
|
234
|
+
/** Final outcome accepted (last verdict APPROVE + result accepted). */
|
|
235
|
+
accepted: boolean
|
|
236
|
+
/** True when the fallback one-shot `delegate()` was used (no continuable seam). */
|
|
237
|
+
fellBackToOneShot?: boolean
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Verdict vocabulary shared by T3/T4 (matches the delegated review schema). */
|
|
241
|
+
export type DelegationVerdict = 'APPROVE' | 'REVISE' | 'REJECT'
|
|
242
|
+
|
|
243
|
+
/** Read the verdict from a review-schema structured result (pure). */
|
|
244
|
+
export function readVerdictFromStructured(result: SubagentResultLike): DelegationVerdict {
|
|
245
|
+
// SAFETY: reviewOutputSchema() defines verdict as a string enum; the cast reads
|
|
246
|
+
// one leaf field only, never mutates, and falls back on a non-matching value.
|
|
247
|
+
const verdict = (result.structured as { verdict?: unknown } | undefined)?.verdict
|
|
248
|
+
if (verdict === 'APPROVE' || verdict === 'REVISE' || verdict === 'REJECT') return verdict
|
|
249
|
+
// No structured verdict: a completed run with text output is a provisional APPROVE
|
|
250
|
+
// candidate, but delegation acceptance stays strict (caller evaluates).
|
|
251
|
+
return 'APPROVE'
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Read the repair instruction from a review-schema structured result (pure). */
|
|
255
|
+
export function readRepairFromStructured(result: SubagentResultLike): string {
|
|
256
|
+
// SAFETY: reviewOutputSchema() defines findings as an array of {severity,title,
|
|
257
|
+
// detail}; the cast reads leaf fields only (no live data, no mutation). The
|
|
258
|
+
// repair instruction is ALWAYS synthesized from the findings — a child cannot
|
|
259
|
+
// inject arbitrary instruction text (prompt-injection hygiene).
|
|
260
|
+
const findings = (result.structured as { findings?: Array<{ title?: string }> } | undefined)?.findings
|
|
261
|
+
const titles = Array.isArray(findings) ? findings.map(f => f.title ?? '').filter(Boolean) : []
|
|
262
|
+
if (titles.length > 0) return 'Address the findings: ' + titles.join('; ')
|
|
263
|
+
return 'REVISE: address the review findings and re-submit.'
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Run a multi-round delegated task on ONE durable continuable child:
|
|
268
|
+
* 1. `startContinuable` (initial prompt) — `start()` is never called.
|
|
269
|
+
* 2. `awaitRoundResult` observes the child's settlement for that round.
|
|
270
|
+
* 3. On REVISE: `followup` delivers the repair instruction to the SAME child.
|
|
271
|
+
* 4. On APPROVE/REJECT: finish (accepted only when the verdict is APPROVE and
|
|
272
|
+
* the result evaluates as accepted).
|
|
273
|
+
*
|
|
274
|
+
* `awaitRoundResult(childId, messageId)` is the ONLY parent-side observation
|
|
275
|
+
* seam: in live usage it waits for the child's settlement notice (the child's
|
|
276
|
+
* `reportFrom` lands in the parent's inbox); in tests it is a fake queue.
|
|
277
|
+
*/
|
|
278
|
+
export async function delegateContinuable(input: {
|
|
279
|
+
subagents: SubagentsRuntimeLike
|
|
280
|
+
provider: string
|
|
281
|
+
label: string
|
|
282
|
+
prompt: string
|
|
283
|
+
parent?: SubagentParentHandle
|
|
284
|
+
toolFilter?: unknown
|
|
285
|
+
maxDepth?: number
|
|
286
|
+
childId?: ContinuableChildId
|
|
287
|
+
maxRounds?: number
|
|
288
|
+
readVerdict?: (result: SubagentResultLike) => DelegationVerdict
|
|
289
|
+
readRepair?: (result: SubagentResultLike) => string | undefined
|
|
290
|
+
awaitRoundResult?: (childId: ContinuableChildId, messageId: ContinuableMessageId) => Promise<SubagentResultLike | null>
|
|
291
|
+
}): Promise<ContinuableDelegationLike> {
|
|
292
|
+
const { subagents, provider, label, prompt, parent, toolFilter, maxDepth } = input
|
|
293
|
+
const maxRounds = input.maxRounds ?? 3
|
|
294
|
+
const readVerdict = input.readVerdict ?? readVerdictFromStructured
|
|
295
|
+
const readRepair = input.readRepair ?? readRepairFromStructured
|
|
296
|
+
|
|
297
|
+
const startContinuable = subagents?.startContinuable
|
|
298
|
+
const followup = subagents?.followup
|
|
299
|
+
// A continuable loop MUST observe the child's real settlement AND hold the
|
|
300
|
+
// exact live parent Agent (the live service authorizes followup by object
|
|
301
|
+
// identity). When either is missing, fall back to one-shot (which returns the
|
|
302
|
+
// actual result) rather than fabricating authority and silently APPROVE-ing.
|
|
303
|
+
const hasContinuableSeam = startContinuable !== undefined && followup !== undefined && input.awaitRoundResult !== undefined
|
|
304
|
+
if (!hasContinuableSeam || subagents === undefined || parent === undefined) {
|
|
305
|
+
// Fall back to one-shot delegation (self-audit-safe): never silently drop.
|
|
306
|
+
try {
|
|
307
|
+
const oneShot = await delegate({
|
|
308
|
+
subagents,
|
|
309
|
+
provider,
|
|
310
|
+
request: {
|
|
311
|
+
prompt: [{ type: 'text', text: prompt }],
|
|
312
|
+
label,
|
|
313
|
+
toolFilter,
|
|
314
|
+
maxDepth,
|
|
315
|
+
parent,
|
|
316
|
+
},
|
|
317
|
+
})
|
|
318
|
+
const verdict = readVerdict(oneShot)
|
|
319
|
+
return {
|
|
320
|
+
ok: true,
|
|
321
|
+
rounds: [{ text: prompt, result: oneShot }],
|
|
322
|
+
accepted: verdict === 'APPROVE' && evaluateDelegationResult(oneShot).accepted,
|
|
323
|
+
fellBackToOneShot: true,
|
|
324
|
+
}
|
|
325
|
+
} catch (err) {
|
|
326
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err), rounds: [], accepted: false, fellBackToOneShot: true }
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const messageIds: ContinuableMessageId[] = []
|
|
331
|
+
const rounds: ContinuableRoundLike[] = []
|
|
332
|
+
let childId: ContinuableChildId | undefined
|
|
333
|
+
|
|
334
|
+
const request: ContinuableStartSpecLike['request'] = {
|
|
335
|
+
prompt: [{ type: 'text', text: prompt }],
|
|
336
|
+
parent,
|
|
337
|
+
}
|
|
338
|
+
if (toolFilter !== undefined) request.toolFilter = toolFilter
|
|
339
|
+
if (maxDepth !== undefined) request.maxDepth = maxDepth
|
|
340
|
+
|
|
341
|
+
const spec: ContinuableStartSpecLike = {
|
|
342
|
+
provider,
|
|
343
|
+
label,
|
|
344
|
+
request,
|
|
345
|
+
}
|
|
346
|
+
if (input.childId !== undefined) spec.childId = input.childId
|
|
347
|
+
try {
|
|
348
|
+
const started = await startContinuable(spec)
|
|
349
|
+
childId = started.childId
|
|
350
|
+
messageIds.push(started.messageId)
|
|
351
|
+
rounds.push({ text: prompt })
|
|
352
|
+
|
|
353
|
+
for (let round = 0; round < maxRounds; round += 1) {
|
|
354
|
+
const current = rounds[round]
|
|
355
|
+
const observed = await input.awaitRoundResult!(childId, messageIds[messageIds.length - 1])
|
|
356
|
+
if (observed === null) {
|
|
357
|
+
return { ok: false, reason: 'continuable child produced no settlement for round ' + (round + 1), childId, messageIds, rounds, accepted: false }
|
|
358
|
+
}
|
|
359
|
+
current.result = observed
|
|
360
|
+
const verdict = readVerdict(observed)
|
|
361
|
+
if (verdict !== 'REVISE') {
|
|
362
|
+
const accepted = verdict === 'APPROVE' && evaluateDelegationResult(observed).accepted
|
|
363
|
+
return {
|
|
364
|
+
ok: accepted,
|
|
365
|
+
reason: accepted ? 'delegation completed' : 'delegation stopped with verdict ' + verdict,
|
|
366
|
+
childId,
|
|
367
|
+
messageIds,
|
|
368
|
+
rounds,
|
|
369
|
+
accepted,
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// REVISE: send the repair instruction to the SAME child (FIFO, context retained).
|
|
373
|
+
const repair = readRepair(observed)
|
|
374
|
+
if (!repair) {
|
|
375
|
+
return { ok: false, reason: 'REVISE verdict without a repair instruction', childId, messageIds, rounds, accepted: false }
|
|
376
|
+
}
|
|
377
|
+
const followupId = await followup(
|
|
378
|
+
parent,
|
|
379
|
+
childId,
|
|
380
|
+
[{ type: 'text', text: repair }],
|
|
381
|
+
{ source: { kind: 'coordinator', form: 'relay', senderSessionId: parent.id ?? '' } },
|
|
382
|
+
)
|
|
383
|
+
messageIds.push(followupId)
|
|
384
|
+
current.revise = true
|
|
385
|
+
current.repair = repair
|
|
386
|
+
rounds.push({ text: repair })
|
|
387
|
+
}
|
|
388
|
+
return { ok: false, reason: 'max rounds reached without an APPROVE', childId, messageIds, rounds, accepted: false }
|
|
389
|
+
} catch (err) {
|
|
390
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
391
|
+
// Failure preserves the child (a later followup may still resume it); the
|
|
392
|
+
// kill switch is explicit (interruptContinuable), never implicit.
|
|
393
|
+
return { ok: false, reason: message, childId, messageIds, rounds, accepted: false }
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* T4 kill switch: interrupt one live continuable child's current turn. Admission
|
|
399
|
+
* is synchronous, the effect asynchronous, and the child's pending inbox is
|
|
400
|
+
* preserved (keepInbox semantics) — a followup later resumes the parked queue.
|
|
401
|
+
*/
|
|
402
|
+
export function interruptContinuable(
|
|
403
|
+
subagents: SubagentsRuntimeLike,
|
|
404
|
+
childId: ContinuableChildId,
|
|
405
|
+
parentSessionId: string,
|
|
406
|
+
): ContinuableOpResult {
|
|
407
|
+
const interrupt = subagents?.interrupt
|
|
408
|
+
if (interrupt === undefined) {
|
|
409
|
+
return { ok: false, reason: 'no continuable interrupt seam' }
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
interrupt(childId, { kind: 'user', parentSessionId })
|
|
413
|
+
return { ok: true }
|
|
414
|
+
} catch (err) {
|
|
415
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) }
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* T4 closeout: release one continuable child (host drains its Activation and
|
|
421
|
+
* disposes its handle). No-op when the seam lacks the method (one-shot hosts).
|
|
422
|
+
*/
|
|
423
|
+
export async function drainContinuableChildren(
|
|
424
|
+
subagents: SubagentsRuntimeLike,
|
|
425
|
+
parent: SubagentParentHandle,
|
|
426
|
+
childIds: readonly ContinuableChildId[],
|
|
427
|
+
): Promise<ContinuableOpResult> {
|
|
428
|
+
if (subagents?.drainContinuableChildren === undefined || childIds.length === 0) return { ok: true }
|
|
429
|
+
try {
|
|
430
|
+
await subagents.drainContinuableChildren(parent, childIds)
|
|
431
|
+
return { ok: true }
|
|
432
|
+
} catch (err) {
|
|
433
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) }
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* T4 closeout (host teardown path): release every continuable descendant below
|
|
439
|
+
* the given live parents (mirrors the live `drainContinuableDescendants`).
|
|
440
|
+
* No-op when the seam lacks the method; the host owns this at session teardown.
|
|
441
|
+
*/
|
|
442
|
+
export async function drainContinuableDescendants(
|
|
443
|
+
subagents: SubagentsRuntimeLike | null,
|
|
444
|
+
parents: readonly SubagentParentHandle[],
|
|
445
|
+
): Promise<ContinuableOpResult> {
|
|
446
|
+
const drain = subagents?.drainContinuableDescendants
|
|
447
|
+
if (drain === undefined || parents.length === 0) return { ok: true }
|
|
448
|
+
try {
|
|
449
|
+
await drain(parents)
|
|
450
|
+
return { ok: true }
|
|
451
|
+
} catch (err) {
|
|
452
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) }
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
119
456
|
export interface Reference {
|
|
120
457
|
path: string
|
|
121
458
|
lineRange?: string
|
|
122
459
|
}
|
|
123
|
-
|
|
124
460
|
export interface ReferenceCheck {
|
|
125
461
|
ok: boolean
|
|
126
462
|
failures: string[]
|
package/src/enforcement.ts
CHANGED
|
@@ -46,7 +46,7 @@ const LOCK_TOOL_NAMES = new Set(['recursive_lock', 'recursive_lock_phase'])
|
|
|
46
46
|
* Pure predicate: inspects the pending tool execution (name + args) against
|
|
47
47
|
* the run tree under the given worktree root.
|
|
48
48
|
*/
|
|
49
|
-
export type ToolGuardDecision = { kind: 'allow' } | { kind: 'deny'; reason: string } | { kind: 'ask'; reason?: string }
|
|
49
|
+
export type ToolGuardDecision = { kind: 'allow'; warn?: string } | { kind: 'deny'; reason: string } | { kind: 'ask'; reason?: string }
|
|
50
50
|
|
|
51
51
|
export interface ToolExecLike {
|
|
52
52
|
name: string
|
|
@@ -108,6 +108,21 @@ export function evaluateToolGuard(
|
|
|
108
108
|
return { kind: 'allow' }
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* T6 (approval ask→policy bridge): an `ask` decision must never be a silent
|
|
113
|
+
* allow. Under `strict` it coerces to `deny`; under `advisory` it stays `allow`
|
|
114
|
+
* but flags a `warn` so the caller never lets it through unlogged. Non-ask
|
|
115
|
+
* decisions pass through unchanged.
|
|
116
|
+
*/
|
|
117
|
+
export function coerceAskToDecision(decision: ToolGuardDecision, mode: EnforcementMode = 'advisory'): ToolGuardDecision {
|
|
118
|
+
if (decision.kind !== 'ask') return decision
|
|
119
|
+
if (mode === 'strict') {
|
|
120
|
+
return { kind: 'deny', reason: decision.reason ?? 'ask under strict enforcement denies' }
|
|
121
|
+
}
|
|
122
|
+
// advisory: allow, but carry the warning so the caller logs (never silent).
|
|
123
|
+
return { kind: 'allow', warn: decision.reason ?? 'ask under advisory enforcement allows' }
|
|
124
|
+
}
|
|
125
|
+
|
|
111
126
|
/** Resolve a tool-target path to an absolute path under the worktree root. */
|
|
112
127
|
function resolveTargetPath(target: string, worktreeRoot: string): string | null {
|
|
113
128
|
const normalized = target.replace(/\\/g, '/')
|