@try-works/dsh-recursive-mode 0.2.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +2 -0
- package/lib/index.js +863 -12
- package/lib/recursive_audit_team.tool.d.ts +2 -0
- package/lib/runtime.d.ts +112 -3
- package/lib/skills.d.ts +70 -0
- package/lib/teams-loop.d.ts +160 -0
- package/package.json +3 -2
- package/scripts/test-recursive-mode-smoke.ts +45 -19
- package/skills/recursive-mode/SKILL.md +66 -0
- package/src/delegation.ts +337 -1
- package/src/enforcement.ts +16 -1
- package/src/goals-projection.ts +149 -0
- package/src/index.ts +50 -11
- package/src/recursive_audit_team.tool.ts +141 -0
- package/src/runtime.ts +134 -6
- package/src/skills.ts +151 -0
- package/src/teams-loop.ts +259 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T3: model the audit→repair→re-audit loop as agentTeams Tasks.
|
|
3
|
+
*
|
|
4
|
+
* The recursion concept's core state machine, expressed over the native
|
|
5
|
+
* `agentTeams` service: ONE durable task per phase carries the loop —
|
|
6
|
+
* createTask (pending) → claim (in_progress) → audit round → on REVISE
|
|
7
|
+
* updateTask(edit, repair instruction) → re-audit the SAME task → on APPROVE
|
|
8
|
+
* updateTask(complete) → lock. A stuck reviewer is interrupted through the
|
|
9
|
+
* team's own kill switch; a REJECT or round-cap releases the task and fails
|
|
10
|
+
* loud — a lock NEVER happens before an APPROVE verdict.
|
|
11
|
+
*
|
|
12
|
+
* Everything here is a structural seam (TeamRuntimeLike / TeamTaskViewLike)
|
|
13
|
+
* so the whole loop is unit-testable with a fake team; the live `agentTeams`
|
|
14
|
+
* service satisfies the seam without an adapter. The caller Agent passes
|
|
15
|
+
* through as an opaque `TeamCallerHandle` (never serialized; workspace-scoped
|
|
16
|
+
* invariant).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Verdict vocabulary shared with T4 (matches the delegated review schema). */
|
|
20
|
+
export type AuditVerdict = 'APPROVE' | 'REVISE' | 'REJECT'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Opaque handle to the live Team member/lead Agent authorizing task mutations.
|
|
24
|
+
* The seam uses it only for identity/authority and never inspects or serializes it.
|
|
25
|
+
*/
|
|
26
|
+
export interface TeamCallerHandle {
|
|
27
|
+
readonly id?: string
|
|
28
|
+
readonly session?: { readonly header?: { readonly cwd?: string } }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Minimal cancellation shape (a live AbortSignal satisfies it). */
|
|
32
|
+
export interface TeamAbortSignalLike {
|
|
33
|
+
readonly throwIfAborted: () => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Task identity: a branded string in the live service. */
|
|
37
|
+
export type TeamTaskIdLike = string
|
|
38
|
+
|
|
39
|
+
/** Task status vocabulary (mirrors the live TeamTaskStatus). */
|
|
40
|
+
export type TeamTaskStatusLike = 'pending' | 'in_progress' | 'completed' | 'deleted'
|
|
41
|
+
|
|
42
|
+
/** Task action vocabulary (mirrors the live TeamTaskAction; the loop uses a subset). */
|
|
43
|
+
export type TeamTaskActionLike = 'claim' | 'release' | 'edit' | 'complete'
|
|
44
|
+
|
|
45
|
+
/** One runtime-enriched task view (the fields the loop reads/writes). */
|
|
46
|
+
export interface TeamTaskViewLike {
|
|
47
|
+
readonly id: TeamTaskIdLike
|
|
48
|
+
readonly revision: number
|
|
49
|
+
readonly subject: string
|
|
50
|
+
readonly description: string
|
|
51
|
+
readonly status: TeamTaskStatusLike
|
|
52
|
+
readonly blockedBy: TeamTaskIdLike[]
|
|
53
|
+
readonly writeScopes: string[]
|
|
54
|
+
readonly ownerName?: string
|
|
55
|
+
readonly ready: boolean
|
|
56
|
+
readonly writeScopeWarnings: string[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A wait observation (mirrors TeamWaitResult). */
|
|
60
|
+
export interface TeamWaitResultLike {
|
|
61
|
+
readonly timedOut: boolean
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Create-task request (mirrors CreateTeamTaskRequest). */
|
|
65
|
+
export interface CreateTeamTaskRequestLike {
|
|
66
|
+
subject: string
|
|
67
|
+
description: string
|
|
68
|
+
blockedBy?: readonly TeamTaskIdLike[]
|
|
69
|
+
writeScopes?: readonly string[]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Compare-and-set task transition (mirrors UpdateTeamTaskRequest). */
|
|
73
|
+
export interface UpdateTeamTaskRequestLike {
|
|
74
|
+
taskId: TeamTaskIdLike
|
|
75
|
+
expectedRevision: number
|
|
76
|
+
action: TeamTaskActionLike
|
|
77
|
+
subject?: string
|
|
78
|
+
description?: string
|
|
79
|
+
blockedBy?: readonly TeamTaskIdLike[]
|
|
80
|
+
writeScopes?: readonly string[]
|
|
81
|
+
owner?: string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The agentTeams seam the loop calls. `waitForChange`/`interrupt`/`getTask`/
|
|
86
|
+
* `listTasks` are optional (loops degrade: no wait, no kill switch, no board
|
|
87
|
+
* re-read) — `createTask`/`updateTask` are hard requirements.
|
|
88
|
+
*/
|
|
89
|
+
export interface TeamRuntimeLike {
|
|
90
|
+
createTask(caller: TeamCallerHandle, request: CreateTeamTaskRequestLike): Promise<TeamTaskViewLike>
|
|
91
|
+
updateTask(caller: TeamCallerHandle, request: UpdateTeamTaskRequestLike): Promise<TeamTaskViewLike>
|
|
92
|
+
getTask?(caller: TeamCallerHandle, id: TeamTaskIdLike): TeamTaskViewLike
|
|
93
|
+
listTasks?(caller: TeamCallerHandle): TeamTaskViewLike[]
|
|
94
|
+
waitForChange?(caller: TeamCallerHandle, timeoutMs: number, signal: TeamAbortSignalLike | undefined): Promise<TeamWaitResultLike>
|
|
95
|
+
interrupt?(caller: TeamCallerHandle, targetName: string): { previousStatus: 'running' | 'idle' | 'inactive' }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** One audit round's outcome (the verdict + synthesized repair + acceptance). */
|
|
99
|
+
export interface AuditRoundOutcome {
|
|
100
|
+
readonly verdict: AuditVerdict
|
|
101
|
+
/** Repair instruction synthesized from findings (REVISE only). */
|
|
102
|
+
readonly repair?: string
|
|
103
|
+
/** Whether the underlying delegation result itself was accepted. */
|
|
104
|
+
readonly accepted: boolean
|
|
105
|
+
readonly reason?: string
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** One completed loop round (the task revision trail for the board history). */
|
|
109
|
+
export interface AuditLoopRound {
|
|
110
|
+
readonly round: number
|
|
111
|
+
readonly verdict: AuditVerdict
|
|
112
|
+
readonly repair?: string
|
|
113
|
+
readonly taskRevision: number
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The auditToPass result. */
|
|
117
|
+
export interface AuditToPassResult {
|
|
118
|
+
readonly ok: boolean
|
|
119
|
+
readonly reason?: string
|
|
120
|
+
/** The durable task the loop ran on (revision trail lives on the team log). */
|
|
121
|
+
readonly taskId?: TeamTaskIdLike
|
|
122
|
+
readonly rounds: AuditLoopRound[]
|
|
123
|
+
/** True only when an APPROVE verdict completed the task and locked the phase. */
|
|
124
|
+
readonly locked: boolean
|
|
125
|
+
/** Latest task view (board-facing per-phase history). */
|
|
126
|
+
readonly taskView?: TeamTaskViewLike
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Inputs for one audit-to-pass loop. */
|
|
130
|
+
export interface AuditToPassInput {
|
|
131
|
+
/** The agentTeams seam. */
|
|
132
|
+
readonly teams: TeamRuntimeLike
|
|
133
|
+
/** Exact live Team member/lead authorizing the task mutations. */
|
|
134
|
+
readonly caller: TeamCallerHandle
|
|
135
|
+
/** Phase + run identity (task subject/description vocabulary). */
|
|
136
|
+
readonly runId: string
|
|
137
|
+
readonly phase: string
|
|
138
|
+
/** Optional task blockers (previous-phase task ids). */
|
|
139
|
+
readonly blockedBy?: readonly TeamTaskIdLike[]
|
|
140
|
+
/** Write scopes for the phase artifact (advisory, overlap-warned). */
|
|
141
|
+
readonly writeScopes?: readonly string[]
|
|
142
|
+
/** Run ONE audit round for the current task; live usage delegates (T4). */
|
|
143
|
+
readonly runAuditRound: (round: number, task: TeamTaskViewLike) => Promise<AuditRoundOutcome>
|
|
144
|
+
/** Lock the phase artifact — called ONLY after an APPROVE verdict. */
|
|
145
|
+
readonly lockPhase: () => Promise<void>
|
|
146
|
+
/** Team member name to interrupt on a stuck reviewer (defaults to the role). */
|
|
147
|
+
readonly reviewerName?: string
|
|
148
|
+
/** Round cap (fail loud past it; no lock). */
|
|
149
|
+
readonly maxRounds?: number
|
|
150
|
+
/** Per-round wait timeout before the audit round runs (skipped without the seam). */
|
|
151
|
+
readonly waitTimeoutMs?: number
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Whether a view is the loop's expected task (guards CAS against foreign ids). */
|
|
155
|
+
function isSameTask(task: TeamTaskViewLike, id: TeamTaskIdLike): boolean {
|
|
156
|
+
return task.id === id
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Render a per-phase task history (board-facing; pure). One line per round plus
|
|
161
|
+
* the final task status — no live data, no mutation.
|
|
162
|
+
*/
|
|
163
|
+
export function renderTaskHistory(task: TeamTaskViewLike | undefined, rounds: readonly AuditLoopRound[]): string {
|
|
164
|
+
const lines: string[] = []
|
|
165
|
+
if (task !== undefined) {
|
|
166
|
+
lines.push('task ' + task.id + ' (' + task.status + ', rev ' + task.revision + '): ' + task.subject)
|
|
167
|
+
}
|
|
168
|
+
for (const round of rounds) {
|
|
169
|
+
const repair = round.repair ? ' — ' + round.repair : ''
|
|
170
|
+
lines.push('round ' + round.round + ': ' + round.verdict + ' (task rev ' + round.taskRevision + ')' + repair)
|
|
171
|
+
}
|
|
172
|
+
return lines.join('\n')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* T3 driver: audit the phase until it passes, on ONE durable team task.
|
|
177
|
+
*
|
|
178
|
+
* Transition trail (the fake records exactly this order):
|
|
179
|
+
* createTask(pending) → claim(in_progress) → waitForChange → audit round
|
|
180
|
+
* → REVISE: updateTask(edit, repair) → waitForChange → re-audit SAME task
|
|
181
|
+
* → APPROVE: updateTask(complete) → lockPhase()
|
|
182
|
+
* → REJECT / cap / stuck: updateTask(release) + interrupt, NO lock.
|
|
183
|
+
*/
|
|
184
|
+
export async function auditToPass(input: AuditToPassInput): Promise<AuditToPassResult> {
|
|
185
|
+
const { teams, caller, runId, phase, runAuditRound, lockPhase } = input
|
|
186
|
+
const maxRounds = input.maxRounds ?? 3
|
|
187
|
+
const waitTimeoutMs = input.waitTimeoutMs ?? 30_000
|
|
188
|
+
const waitForChange = teams.waitForChange
|
|
189
|
+
const interrupt = teams.interrupt
|
|
190
|
+
const rounds: AuditLoopRound[] = []
|
|
191
|
+
|
|
192
|
+
const createRequest: CreateTeamTaskRequestLike = {
|
|
193
|
+
subject: 'Audit to pass: ' + runId + ' ' + phase,
|
|
194
|
+
description: 'drive ' + phase + ' through draft → audit → repair → re-audit → pass → lock for run ' + runId,
|
|
195
|
+
}
|
|
196
|
+
if (input.blockedBy !== undefined) createRequest.blockedBy = input.blockedBy
|
|
197
|
+
if (input.writeScopes !== undefined) createRequest.writeScopes = input.writeScopes
|
|
198
|
+
const task = await teams.createTask(caller, createRequest)
|
|
199
|
+
|
|
200
|
+
const claim = await teams.updateTask(caller, { taskId: task.id, expectedRevision: task.revision, action: 'claim' })
|
|
201
|
+
if (!isSameTask(claim, task.id)) return { ok: false, reason: 'claim returned a foreign task', taskId: task.id, rounds, locked: false, taskView: claim }
|
|
202
|
+
|
|
203
|
+
let current = claim
|
|
204
|
+
rounds.push({ round: 0, verdict: 'REVISE', taskRevision: current.revision })
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
for (let round = 1; round <= maxRounds; round += 1) {
|
|
208
|
+
// Wait for team activity before the round (bounded; skipped without the seam).
|
|
209
|
+
if (waitForChange !== undefined) {
|
|
210
|
+
await waitForChange(caller, waitTimeoutMs, undefined)
|
|
211
|
+
}
|
|
212
|
+
const outcome = await runAuditRound(round, current)
|
|
213
|
+
if (!isSameTask(current, task.id)) return { ok: false, reason: 'round observed a foreign task', taskId: task.id, rounds, locked: false, taskView: current }
|
|
214
|
+
|
|
215
|
+
if (outcome.verdict === 'APPROVE') {
|
|
216
|
+
const completed = await teams.updateTask(caller, { taskId: task.id, expectedRevision: current.revision, action: 'complete' })
|
|
217
|
+
await lockPhase()
|
|
218
|
+
rounds.push({ round, verdict: 'APPROVE', taskRevision: completed.revision })
|
|
219
|
+
return { ok: outcome.accepted, reason: outcome.accepted ? 'audit passed and phase locked' : 'verdict APPROVE but delegation not accepted', taskId: task.id, rounds, locked: true, taskView: completed }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (outcome.verdict === 'REJECT') {
|
|
223
|
+
// A rejected audit is not a pass: release the task and fail loud.
|
|
224
|
+
const released = await teams.updateTask(caller, { taskId: task.id, expectedRevision: current.revision, action: 'release' })
|
|
225
|
+
rounds.push({ round, verdict: 'REJECT', taskRevision: released.revision })
|
|
226
|
+
return { ok: false, reason: 'audit rejected at round ' + round, taskId: task.id, rounds, locked: false, taskView: released }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// REVISE: record the repair instruction on the SAME task and re-audit.
|
|
230
|
+
const repair = outcome.repair ?? 'REVISE: address the review findings and re-submit.'
|
|
231
|
+
const edited = await teams.updateTask(caller, {
|
|
232
|
+
taskId: task.id,
|
|
233
|
+
expectedRevision: current.revision,
|
|
234
|
+
action: 'edit',
|
|
235
|
+
description: current.description + '\nround ' + round + ' repair: ' + repair,
|
|
236
|
+
})
|
|
237
|
+
current = edited
|
|
238
|
+
rounds.push({ round, verdict: 'REVISE', repair, taskRevision: edited.revision })
|
|
239
|
+
}
|
|
240
|
+
// Round cap: release and fail loud — never lock without an APPROVE.
|
|
241
|
+
const released = await teams.updateTask(caller, { taskId: task.id, expectedRevision: current.revision, action: 'release' })
|
|
242
|
+
return { ok: false, reason: 'max rounds reached without an APPROVE', taskId: task.id, rounds, locked: false, taskView: released }
|
|
243
|
+
} catch (err) {
|
|
244
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
245
|
+
// A stuck reviewer is interrupted through the team kill switch (the task
|
|
246
|
+
// itself stays for a later resume); best-effort, never replaces the error.
|
|
247
|
+
if (interrupt !== undefined) {
|
|
248
|
+
try {
|
|
249
|
+
interrupt(caller, input.reviewerName ?? 'auditor')
|
|
250
|
+
} catch { /* interrupt is best-effort */ }
|
|
251
|
+
}
|
|
252
|
+
return { ok: false, reason: message, taskId: task.id, rounds, locked: false, taskView: current }
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Whether a task view is currently claimed by the named owner (board-facing). */
|
|
257
|
+
export function isTaskClaimedBy(task: TeamTaskViewLike, ownerName: string): boolean {
|
|
258
|
+
return task.ownerName === ownerName && task.status === 'in_progress'
|
|
259
|
+
}
|