@try-works/dsh-recursive-mode 0.2.4 → 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/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 +773 -9
- 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/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/recursive_audit_team.tool.ts +141 -0
- package/src/runtime.ts +134 -6
- package/src/teams-loop.ts +259 -0
package/lib/index.js
CHANGED
|
@@ -4316,6 +4316,218 @@ async function delegate(input) {
|
|
|
4316
4316
|
throw delegationError(message, "DELEGATION_FAILED");
|
|
4317
4317
|
}
|
|
4318
4318
|
}
|
|
4319
|
+
/** Read the verdict from a review-schema structured result (pure). */
|
|
4320
|
+
function readVerdictFromStructured(result) {
|
|
4321
|
+
const verdict = result.structured?.verdict;
|
|
4322
|
+
if (verdict === "APPROVE" || verdict === "REVISE" || verdict === "REJECT") return verdict;
|
|
4323
|
+
return "APPROVE";
|
|
4324
|
+
}
|
|
4325
|
+
/** Read the repair instruction from a review-schema structured result (pure). */
|
|
4326
|
+
function readRepairFromStructured(result) {
|
|
4327
|
+
const findings = result.structured?.findings;
|
|
4328
|
+
const titles = Array.isArray(findings) ? findings.map((f) => f.title ?? "").filter(Boolean) : [];
|
|
4329
|
+
if (titles.length > 0) return "Address the findings: " + titles.join("; ");
|
|
4330
|
+
return "REVISE: address the review findings and re-submit.";
|
|
4331
|
+
}
|
|
4332
|
+
/**
|
|
4333
|
+
* Run a multi-round delegated task on ONE durable continuable child:
|
|
4334
|
+
* 1. `startContinuable` (initial prompt) — `start()` is never called.
|
|
4335
|
+
* 2. `awaitRoundResult` observes the child's settlement for that round.
|
|
4336
|
+
* 3. On REVISE: `followup` delivers the repair instruction to the SAME child.
|
|
4337
|
+
* 4. On APPROVE/REJECT: finish (accepted only when the verdict is APPROVE and
|
|
4338
|
+
* the result evaluates as accepted).
|
|
4339
|
+
*
|
|
4340
|
+
* `awaitRoundResult(childId, messageId)` is the ONLY parent-side observation
|
|
4341
|
+
* seam: in live usage it waits for the child's settlement notice (the child's
|
|
4342
|
+
* `reportFrom` lands in the parent's inbox); in tests it is a fake queue.
|
|
4343
|
+
*/
|
|
4344
|
+
async function delegateContinuable(input) {
|
|
4345
|
+
const { subagents, provider, label, prompt, parent, toolFilter, maxDepth } = input;
|
|
4346
|
+
const maxRounds = input.maxRounds ?? 3;
|
|
4347
|
+
const readVerdict = input.readVerdict ?? readVerdictFromStructured;
|
|
4348
|
+
const readRepair = input.readRepair ?? readRepairFromStructured;
|
|
4349
|
+
const startContinuable = subagents?.startContinuable;
|
|
4350
|
+
const followup = subagents?.followup;
|
|
4351
|
+
if (!(startContinuable !== void 0 && followup !== void 0 && input.awaitRoundResult !== void 0) || subagents === void 0 || parent === void 0) try {
|
|
4352
|
+
const oneShot = await delegate({
|
|
4353
|
+
subagents,
|
|
4354
|
+
provider,
|
|
4355
|
+
request: {
|
|
4356
|
+
prompt: [{
|
|
4357
|
+
type: "text",
|
|
4358
|
+
text: prompt
|
|
4359
|
+
}],
|
|
4360
|
+
label,
|
|
4361
|
+
toolFilter,
|
|
4362
|
+
maxDepth,
|
|
4363
|
+
parent
|
|
4364
|
+
}
|
|
4365
|
+
});
|
|
4366
|
+
const verdict = readVerdict(oneShot);
|
|
4367
|
+
return {
|
|
4368
|
+
ok: true,
|
|
4369
|
+
rounds: [{
|
|
4370
|
+
text: prompt,
|
|
4371
|
+
result: oneShot
|
|
4372
|
+
}],
|
|
4373
|
+
accepted: verdict === "APPROVE" && evaluateDelegationResult(oneShot).accepted,
|
|
4374
|
+
fellBackToOneShot: true
|
|
4375
|
+
};
|
|
4376
|
+
} catch (err) {
|
|
4377
|
+
return {
|
|
4378
|
+
ok: false,
|
|
4379
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
4380
|
+
rounds: [],
|
|
4381
|
+
accepted: false,
|
|
4382
|
+
fellBackToOneShot: true
|
|
4383
|
+
};
|
|
4384
|
+
}
|
|
4385
|
+
const messageIds = [];
|
|
4386
|
+
const rounds = [];
|
|
4387
|
+
let childId;
|
|
4388
|
+
const request = {
|
|
4389
|
+
prompt: [{
|
|
4390
|
+
type: "text",
|
|
4391
|
+
text: prompt
|
|
4392
|
+
}],
|
|
4393
|
+
parent
|
|
4394
|
+
};
|
|
4395
|
+
if (toolFilter !== void 0) request.toolFilter = toolFilter;
|
|
4396
|
+
if (maxDepth !== void 0) request.maxDepth = maxDepth;
|
|
4397
|
+
const spec = {
|
|
4398
|
+
provider,
|
|
4399
|
+
label,
|
|
4400
|
+
request
|
|
4401
|
+
};
|
|
4402
|
+
if (input.childId !== void 0) spec.childId = input.childId;
|
|
4403
|
+
try {
|
|
4404
|
+
const started = await startContinuable(spec);
|
|
4405
|
+
childId = started.childId;
|
|
4406
|
+
messageIds.push(started.messageId);
|
|
4407
|
+
rounds.push({ text: prompt });
|
|
4408
|
+
for (let round = 0; round < maxRounds; round += 1) {
|
|
4409
|
+
const current = rounds[round];
|
|
4410
|
+
const observed = await input.awaitRoundResult(childId, messageIds[messageIds.length - 1]);
|
|
4411
|
+
if (observed === null) return {
|
|
4412
|
+
ok: false,
|
|
4413
|
+
reason: "continuable child produced no settlement for round " + (round + 1),
|
|
4414
|
+
childId,
|
|
4415
|
+
messageIds,
|
|
4416
|
+
rounds,
|
|
4417
|
+
accepted: false
|
|
4418
|
+
};
|
|
4419
|
+
current.result = observed;
|
|
4420
|
+
const verdict = readVerdict(observed);
|
|
4421
|
+
if (verdict !== "REVISE") {
|
|
4422
|
+
const accepted = verdict === "APPROVE" && evaluateDelegationResult(observed).accepted;
|
|
4423
|
+
return {
|
|
4424
|
+
ok: accepted,
|
|
4425
|
+
reason: accepted ? "delegation completed" : "delegation stopped with verdict " + verdict,
|
|
4426
|
+
childId,
|
|
4427
|
+
messageIds,
|
|
4428
|
+
rounds,
|
|
4429
|
+
accepted
|
|
4430
|
+
};
|
|
4431
|
+
}
|
|
4432
|
+
const repair = readRepair(observed);
|
|
4433
|
+
if (!repair) return {
|
|
4434
|
+
ok: false,
|
|
4435
|
+
reason: "REVISE verdict without a repair instruction",
|
|
4436
|
+
childId,
|
|
4437
|
+
messageIds,
|
|
4438
|
+
rounds,
|
|
4439
|
+
accepted: false
|
|
4440
|
+
};
|
|
4441
|
+
const followupId = await followup(parent, childId, [{
|
|
4442
|
+
type: "text",
|
|
4443
|
+
text: repair
|
|
4444
|
+
}], { source: {
|
|
4445
|
+
kind: "coordinator",
|
|
4446
|
+
form: "relay",
|
|
4447
|
+
senderSessionId: parent.id ?? ""
|
|
4448
|
+
} });
|
|
4449
|
+
messageIds.push(followupId);
|
|
4450
|
+
current.revise = true;
|
|
4451
|
+
current.repair = repair;
|
|
4452
|
+
rounds.push({ text: repair });
|
|
4453
|
+
}
|
|
4454
|
+
return {
|
|
4455
|
+
ok: false,
|
|
4456
|
+
reason: "max rounds reached without an APPROVE",
|
|
4457
|
+
childId,
|
|
4458
|
+
messageIds,
|
|
4459
|
+
rounds,
|
|
4460
|
+
accepted: false
|
|
4461
|
+
};
|
|
4462
|
+
} catch (err) {
|
|
4463
|
+
return {
|
|
4464
|
+
ok: false,
|
|
4465
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
4466
|
+
childId,
|
|
4467
|
+
messageIds,
|
|
4468
|
+
rounds,
|
|
4469
|
+
accepted: false
|
|
4470
|
+
};
|
|
4471
|
+
}
|
|
4472
|
+
}
|
|
4473
|
+
/**
|
|
4474
|
+
* T4 kill switch: interrupt one live continuable child's current turn. Admission
|
|
4475
|
+
* is synchronous, the effect asynchronous, and the child's pending inbox is
|
|
4476
|
+
* preserved (keepInbox semantics) — a followup later resumes the parked queue.
|
|
4477
|
+
*/
|
|
4478
|
+
function interruptContinuable(subagents, childId, parentSessionId) {
|
|
4479
|
+
const interrupt = subagents?.interrupt;
|
|
4480
|
+
if (interrupt === void 0) return {
|
|
4481
|
+
ok: false,
|
|
4482
|
+
reason: "no continuable interrupt seam"
|
|
4483
|
+
};
|
|
4484
|
+
try {
|
|
4485
|
+
interrupt(childId, {
|
|
4486
|
+
kind: "user",
|
|
4487
|
+
parentSessionId
|
|
4488
|
+
});
|
|
4489
|
+
return { ok: true };
|
|
4490
|
+
} catch (err) {
|
|
4491
|
+
return {
|
|
4492
|
+
ok: false,
|
|
4493
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
4494
|
+
};
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
/**
|
|
4498
|
+
* T4 closeout: release one continuable child (host drains its Activation and
|
|
4499
|
+
* disposes its handle). No-op when the seam lacks the method (one-shot hosts).
|
|
4500
|
+
*/
|
|
4501
|
+
async function drainContinuableChildren(subagents, parent, childIds) {
|
|
4502
|
+
if (subagents?.drainContinuableChildren === void 0 || childIds.length === 0) return { ok: true };
|
|
4503
|
+
try {
|
|
4504
|
+
await subagents.drainContinuableChildren(parent, childIds);
|
|
4505
|
+
return { ok: true };
|
|
4506
|
+
} catch (err) {
|
|
4507
|
+
return {
|
|
4508
|
+
ok: false,
|
|
4509
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
4510
|
+
};
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
/**
|
|
4514
|
+
* T4 closeout (host teardown path): release every continuable descendant below
|
|
4515
|
+
* the given live parents (mirrors the live `drainContinuableDescendants`).
|
|
4516
|
+
* No-op when the seam lacks the method; the host owns this at session teardown.
|
|
4517
|
+
*/
|
|
4518
|
+
async function drainContinuableDescendants(subagents, parents) {
|
|
4519
|
+
const drain = subagents?.drainContinuableDescendants;
|
|
4520
|
+
if (drain === void 0 || parents.length === 0) return { ok: true };
|
|
4521
|
+
try {
|
|
4522
|
+
await drain(parents);
|
|
4523
|
+
return { ok: true };
|
|
4524
|
+
} catch (err) {
|
|
4525
|
+
return {
|
|
4526
|
+
ok: false,
|
|
4527
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
4528
|
+
};
|
|
4529
|
+
}
|
|
4530
|
+
}
|
|
4319
4531
|
function norm(repoRelative) {
|
|
4320
4532
|
return repoRelative.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
4321
4533
|
}
|
|
@@ -4690,6 +4902,23 @@ function evaluateToolGuard(exec, worktreeRoot, activeRunId, mode = "advisory") {
|
|
|
4690
4902
|
}
|
|
4691
4903
|
return { kind: "allow" };
|
|
4692
4904
|
}
|
|
4905
|
+
/**
|
|
4906
|
+
* T6 (approval ask→policy bridge): an `ask` decision must never be a silent
|
|
4907
|
+
* allow. Under `strict` it coerces to `deny`; under `advisory` it stays `allow`
|
|
4908
|
+
* but flags a `warn` so the caller never lets it through unlogged. Non-ask
|
|
4909
|
+
* decisions pass through unchanged.
|
|
4910
|
+
*/
|
|
4911
|
+
function coerceAskToDecision(decision, mode = "advisory") {
|
|
4912
|
+
if (decision.kind !== "ask") return decision;
|
|
4913
|
+
if (mode === "strict") return {
|
|
4914
|
+
kind: "deny",
|
|
4915
|
+
reason: decision.reason ?? "ask under strict enforcement denies"
|
|
4916
|
+
};
|
|
4917
|
+
return {
|
|
4918
|
+
kind: "allow",
|
|
4919
|
+
warn: decision.reason ?? "ask under advisory enforcement allows"
|
|
4920
|
+
};
|
|
4921
|
+
}
|
|
4693
4922
|
/** Resolve a tool-target path to an absolute path under the worktree root. */
|
|
4694
4923
|
function resolveTargetPath(target, worktreeRoot) {
|
|
4695
4924
|
const normalized = target.replace(/\\/g, "/");
|
|
@@ -5055,6 +5284,291 @@ function promoteBranch(opts) {
|
|
|
5055
5284
|
}
|
|
5056
5285
|
}
|
|
5057
5286
|
//#endregion
|
|
5287
|
+
//#region src/goals-projection.ts
|
|
5288
|
+
/** Marker embedded in the goal objective so a goal can be matched to its run. */
|
|
5289
|
+
function runGoalTag(runId) {
|
|
5290
|
+
return "recursive-run:" + runId;
|
|
5291
|
+
}
|
|
5292
|
+
/** The durable objective string for a run goal. */
|
|
5293
|
+
function goalObjective(runId, runState = "active") {
|
|
5294
|
+
return runGoalTag(runId) + " · " + runState;
|
|
5295
|
+
}
|
|
5296
|
+
/** Map a run state onto the native goal phase it should project to. */
|
|
5297
|
+
const RUN_TO_GOAL_PHASE = {
|
|
5298
|
+
new: "active",
|
|
5299
|
+
active: "active",
|
|
5300
|
+
paused: "paused",
|
|
5301
|
+
blocked: "blocked",
|
|
5302
|
+
complete: "complete"
|
|
5303
|
+
};
|
|
5304
|
+
/** Is this goal's objective the marker for `runId`? */
|
|
5305
|
+
function isRunGoal(goal, runId) {
|
|
5306
|
+
return goal?.objective?.startsWith(runGoalTag(runId)) === true;
|
|
5307
|
+
}
|
|
5308
|
+
/** Read a live ref (id + revision) for a goal. */
|
|
5309
|
+
function refOf(goal) {
|
|
5310
|
+
return {
|
|
5311
|
+
id: goal.id,
|
|
5312
|
+
revision: goal.revision
|
|
5313
|
+
};
|
|
5314
|
+
}
|
|
5315
|
+
/** Commit a phase mutation; the real service returns a truthy GoalView on success. */
|
|
5316
|
+
function mutatePhase(service, agent, ref, target) {
|
|
5317
|
+
switch (target) {
|
|
5318
|
+
case "blocked": return !!service.block(agent, ref, {
|
|
5319
|
+
code: "run-gate-block",
|
|
5320
|
+
message: "recursive run gate block"
|
|
5321
|
+
});
|
|
5322
|
+
case "paused": return !!service.pause(agent, ref);
|
|
5323
|
+
case "complete": return !!service.complete(agent, ref);
|
|
5324
|
+
case "active": return !!service.resume(agent, ref);
|
|
5325
|
+
}
|
|
5326
|
+
}
|
|
5327
|
+
/**
|
|
5328
|
+
* Sync a run's durable goal to the requested phase. Safe: never touches a goal
|
|
5329
|
+
* whose objective is not this run's marker, and never re-creates over a
|
|
5330
|
+
* non-complete foreign goal.
|
|
5331
|
+
*/
|
|
5332
|
+
function syncRunGoal(service, agent, runId, runState) {
|
|
5333
|
+
if (!service) return {
|
|
5334
|
+
ok: false,
|
|
5335
|
+
reason: "no goals service"
|
|
5336
|
+
};
|
|
5337
|
+
const target = RUN_TO_GOAL_PHASE[runState];
|
|
5338
|
+
const current = service.get(agent);
|
|
5339
|
+
if (current && isRunGoal(current, runId)) {
|
|
5340
|
+
const phase = current.phase ?? "active";
|
|
5341
|
+
const ref = refOf(current);
|
|
5342
|
+
if (phase === target) return {
|
|
5343
|
+
ok: true,
|
|
5344
|
+
phase: target,
|
|
5345
|
+
ref
|
|
5346
|
+
};
|
|
5347
|
+
if (phase === "complete") return {
|
|
5348
|
+
ok: true,
|
|
5349
|
+
phase: target,
|
|
5350
|
+
ref: refOf(service.create(agent, { objective: goalObjective(runId, runState) })),
|
|
5351
|
+
created: true
|
|
5352
|
+
};
|
|
5353
|
+
return mutatePhase(service, agent, ref, target) ? {
|
|
5354
|
+
ok: true,
|
|
5355
|
+
phase: target,
|
|
5356
|
+
ref
|
|
5357
|
+
} : {
|
|
5358
|
+
ok: false,
|
|
5359
|
+
reason: "goal mutation failed"
|
|
5360
|
+
};
|
|
5361
|
+
}
|
|
5362
|
+
if (current) {
|
|
5363
|
+
if (current.phase === "complete") return {
|
|
5364
|
+
ok: true,
|
|
5365
|
+
phase: target,
|
|
5366
|
+
ref: refOf(service.create(agent, { objective: goalObjective(runId, runState) })),
|
|
5367
|
+
created: true
|
|
5368
|
+
};
|
|
5369
|
+
return {
|
|
5370
|
+
ok: false,
|
|
5371
|
+
reason: "a non-matching active goal exists (foreign goal not touched)"
|
|
5372
|
+
};
|
|
5373
|
+
}
|
|
5374
|
+
return {
|
|
5375
|
+
ok: true,
|
|
5376
|
+
phase: target,
|
|
5377
|
+
ref: refOf(service.create(agent, { objective: goalObjective(runId, runState) })),
|
|
5378
|
+
created: true
|
|
5379
|
+
};
|
|
5380
|
+
}
|
|
5381
|
+
/** Block the current run goal (used on a gate-block). Never touches a foreign goal. */
|
|
5382
|
+
function blockRunGoal(service, agent, runId, reason) {
|
|
5383
|
+
if (!service) return {
|
|
5384
|
+
ok: false,
|
|
5385
|
+
reason: "no goals service"
|
|
5386
|
+
};
|
|
5387
|
+
const current = service.get(agent);
|
|
5388
|
+
if (!current) return {
|
|
5389
|
+
ok: false,
|
|
5390
|
+
reason: "no current goal to block"
|
|
5391
|
+
};
|
|
5392
|
+
if (!isRunGoal(current, runId)) return {
|
|
5393
|
+
ok: false,
|
|
5394
|
+
reason: "current goal is not for this run (foreign goal not touched)"
|
|
5395
|
+
};
|
|
5396
|
+
const ref = refOf(current);
|
|
5397
|
+
return !!service.block(agent, ref, reason) ? {
|
|
5398
|
+
ok: true,
|
|
5399
|
+
phase: "blocked",
|
|
5400
|
+
ref
|
|
5401
|
+
} : {
|
|
5402
|
+
ok: false,
|
|
5403
|
+
reason: "goal block failed"
|
|
5404
|
+
};
|
|
5405
|
+
}
|
|
5406
|
+
/** Bridge a run's blocked goal back to active (used on a reopen). */
|
|
5407
|
+
function resumeRunGoal(service, agent, runId) {
|
|
5408
|
+
return syncRunGoal(service, agent, runId, "active");
|
|
5409
|
+
}
|
|
5410
|
+
//#endregion
|
|
5411
|
+
//#region src/teams-loop.ts
|
|
5412
|
+
/** Whether a view is the loop's expected task (guards CAS against foreign ids). */
|
|
5413
|
+
function isSameTask(task, id) {
|
|
5414
|
+
return task.id === id;
|
|
5415
|
+
}
|
|
5416
|
+
/**
|
|
5417
|
+
* Render a per-phase task history (board-facing; pure). One line per round plus
|
|
5418
|
+
* the final task status — no live data, no mutation.
|
|
5419
|
+
*/
|
|
5420
|
+
function renderTaskHistory(task, rounds) {
|
|
5421
|
+
const lines = [];
|
|
5422
|
+
if (task !== void 0) lines.push("task " + task.id + " (" + task.status + ", rev " + task.revision + "): " + task.subject);
|
|
5423
|
+
for (const round of rounds) {
|
|
5424
|
+
const repair = round.repair ? " — " + round.repair : "";
|
|
5425
|
+
lines.push("round " + round.round + ": " + round.verdict + " (task rev " + round.taskRevision + ")" + repair);
|
|
5426
|
+
}
|
|
5427
|
+
return lines.join("\n");
|
|
5428
|
+
}
|
|
5429
|
+
/**
|
|
5430
|
+
* T3 driver: audit the phase until it passes, on ONE durable team task.
|
|
5431
|
+
*
|
|
5432
|
+
* Transition trail (the fake records exactly this order):
|
|
5433
|
+
* createTask(pending) → claim(in_progress) → waitForChange → audit round
|
|
5434
|
+
* → REVISE: updateTask(edit, repair) → waitForChange → re-audit SAME task
|
|
5435
|
+
* → APPROVE: updateTask(complete) → lockPhase()
|
|
5436
|
+
* → REJECT / cap / stuck: updateTask(release) + interrupt, NO lock.
|
|
5437
|
+
*/
|
|
5438
|
+
async function auditToPass(input) {
|
|
5439
|
+
const { teams, caller, runId, phase, runAuditRound, lockPhase } = input;
|
|
5440
|
+
const maxRounds = input.maxRounds ?? 3;
|
|
5441
|
+
const waitTimeoutMs = input.waitTimeoutMs ?? 3e4;
|
|
5442
|
+
const waitForChange = teams.waitForChange;
|
|
5443
|
+
const interrupt = teams.interrupt;
|
|
5444
|
+
const rounds = [];
|
|
5445
|
+
const createRequest = {
|
|
5446
|
+
subject: "Audit to pass: " + runId + " " + phase,
|
|
5447
|
+
description: "drive " + phase + " through draft → audit → repair → re-audit → pass → lock for run " + runId
|
|
5448
|
+
};
|
|
5449
|
+
if (input.blockedBy !== void 0) createRequest.blockedBy = input.blockedBy;
|
|
5450
|
+
if (input.writeScopes !== void 0) createRequest.writeScopes = input.writeScopes;
|
|
5451
|
+
const task = await teams.createTask(caller, createRequest);
|
|
5452
|
+
const claim = await teams.updateTask(caller, {
|
|
5453
|
+
taskId: task.id,
|
|
5454
|
+
expectedRevision: task.revision,
|
|
5455
|
+
action: "claim"
|
|
5456
|
+
});
|
|
5457
|
+
if (!isSameTask(claim, task.id)) return {
|
|
5458
|
+
ok: false,
|
|
5459
|
+
reason: "claim returned a foreign task",
|
|
5460
|
+
taskId: task.id,
|
|
5461
|
+
rounds,
|
|
5462
|
+
locked: false,
|
|
5463
|
+
taskView: claim
|
|
5464
|
+
};
|
|
5465
|
+
let current = claim;
|
|
5466
|
+
rounds.push({
|
|
5467
|
+
round: 0,
|
|
5468
|
+
verdict: "REVISE",
|
|
5469
|
+
taskRevision: current.revision
|
|
5470
|
+
});
|
|
5471
|
+
try {
|
|
5472
|
+
for (let round = 1; round <= maxRounds; round += 1) {
|
|
5473
|
+
if (waitForChange !== void 0) await waitForChange(caller, waitTimeoutMs, void 0);
|
|
5474
|
+
const outcome = await runAuditRound(round, current);
|
|
5475
|
+
if (!isSameTask(current, task.id)) return {
|
|
5476
|
+
ok: false,
|
|
5477
|
+
reason: "round observed a foreign task",
|
|
5478
|
+
taskId: task.id,
|
|
5479
|
+
rounds,
|
|
5480
|
+
locked: false,
|
|
5481
|
+
taskView: current
|
|
5482
|
+
};
|
|
5483
|
+
if (outcome.verdict === "APPROVE") {
|
|
5484
|
+
const completed = await teams.updateTask(caller, {
|
|
5485
|
+
taskId: task.id,
|
|
5486
|
+
expectedRevision: current.revision,
|
|
5487
|
+
action: "complete"
|
|
5488
|
+
});
|
|
5489
|
+
await lockPhase();
|
|
5490
|
+
rounds.push({
|
|
5491
|
+
round,
|
|
5492
|
+
verdict: "APPROVE",
|
|
5493
|
+
taskRevision: completed.revision
|
|
5494
|
+
});
|
|
5495
|
+
return {
|
|
5496
|
+
ok: outcome.accepted,
|
|
5497
|
+
reason: outcome.accepted ? "audit passed and phase locked" : "verdict APPROVE but delegation not accepted",
|
|
5498
|
+
taskId: task.id,
|
|
5499
|
+
rounds,
|
|
5500
|
+
locked: true,
|
|
5501
|
+
taskView: completed
|
|
5502
|
+
};
|
|
5503
|
+
}
|
|
5504
|
+
if (outcome.verdict === "REJECT") {
|
|
5505
|
+
const released = await teams.updateTask(caller, {
|
|
5506
|
+
taskId: task.id,
|
|
5507
|
+
expectedRevision: current.revision,
|
|
5508
|
+
action: "release"
|
|
5509
|
+
});
|
|
5510
|
+
rounds.push({
|
|
5511
|
+
round,
|
|
5512
|
+
verdict: "REJECT",
|
|
5513
|
+
taskRevision: released.revision
|
|
5514
|
+
});
|
|
5515
|
+
return {
|
|
5516
|
+
ok: false,
|
|
5517
|
+
reason: "audit rejected at round " + round,
|
|
5518
|
+
taskId: task.id,
|
|
5519
|
+
rounds,
|
|
5520
|
+
locked: false,
|
|
5521
|
+
taskView: released
|
|
5522
|
+
};
|
|
5523
|
+
}
|
|
5524
|
+
const repair = outcome.repair ?? "REVISE: address the review findings and re-submit.";
|
|
5525
|
+
const edited = await teams.updateTask(caller, {
|
|
5526
|
+
taskId: task.id,
|
|
5527
|
+
expectedRevision: current.revision,
|
|
5528
|
+
action: "edit",
|
|
5529
|
+
description: current.description + "\nround " + round + " repair: " + repair
|
|
5530
|
+
});
|
|
5531
|
+
current = edited;
|
|
5532
|
+
rounds.push({
|
|
5533
|
+
round,
|
|
5534
|
+
verdict: "REVISE",
|
|
5535
|
+
repair,
|
|
5536
|
+
taskRevision: edited.revision
|
|
5537
|
+
});
|
|
5538
|
+
}
|
|
5539
|
+
const released = await teams.updateTask(caller, {
|
|
5540
|
+
taskId: task.id,
|
|
5541
|
+
expectedRevision: current.revision,
|
|
5542
|
+
action: "release"
|
|
5543
|
+
});
|
|
5544
|
+
return {
|
|
5545
|
+
ok: false,
|
|
5546
|
+
reason: "max rounds reached without an APPROVE",
|
|
5547
|
+
taskId: task.id,
|
|
5548
|
+
rounds,
|
|
5549
|
+
locked: false,
|
|
5550
|
+
taskView: released
|
|
5551
|
+
};
|
|
5552
|
+
} catch (err) {
|
|
5553
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5554
|
+
if (interrupt !== void 0) try {
|
|
5555
|
+
interrupt(caller, input.reviewerName ?? "auditor");
|
|
5556
|
+
} catch {}
|
|
5557
|
+
return {
|
|
5558
|
+
ok: false,
|
|
5559
|
+
reason: message,
|
|
5560
|
+
taskId: task.id,
|
|
5561
|
+
rounds,
|
|
5562
|
+
locked: false,
|
|
5563
|
+
taskView: current
|
|
5564
|
+
};
|
|
5565
|
+
}
|
|
5566
|
+
}
|
|
5567
|
+
/** Whether a task view is currently claimed by the named owner (board-facing). */
|
|
5568
|
+
function isTaskClaimedBy(task, ownerName) {
|
|
5569
|
+
return task.ownerName === ownerName && task.status === "in_progress";
|
|
5570
|
+
}
|
|
5571
|
+
//#endregion
|
|
5058
5572
|
//#region src/runtime.ts
|
|
5059
5573
|
var RecursiveRuntime = class extends Service {
|
|
5060
5574
|
/** Recursive-mode runtime service. Owns run-state reads + lock/init/lint operations. */
|
|
@@ -5062,11 +5576,74 @@ var RecursiveRuntime = class extends Service {
|
|
|
5062
5576
|
super(ctx, "recursive");
|
|
5063
5577
|
this.repoRoot = config.repoRoot ?? process.cwd();
|
|
5064
5578
|
this.workspaceRegistry = config.workspaceRegistry ?? null;
|
|
5579
|
+
this.goalsService = config.goals ?? null;
|
|
5065
5580
|
}
|
|
5066
5581
|
repoRoot;
|
|
5067
5582
|
workspaceRegistry;
|
|
5583
|
+
goalsService;
|
|
5068
5584
|
_enforcementConfig = null;
|
|
5069
5585
|
/**
|
|
5586
|
+
* T3 (agentTeams task loop): run the audit→repair→re-audit state machine on
|
|
5587
|
+
* ONE durable team task. The `teams` seam (live `ctx.agentTeams`) is injected
|
|
5588
|
+
* per-call so the loop stays unit-testable; `runAuditRound` is the caller's
|
|
5589
|
+
* round executor (live usage wires T4's continuable delegation). Locking the
|
|
5590
|
+
* phase artifact is `lockPhase` — the loop NEVER locks before an APPROVE.
|
|
5591
|
+
*/
|
|
5592
|
+
async auditToPass(input) {
|
|
5593
|
+
const { teams, caller, runId, phase, artifact, runAuditRound, agent } = input;
|
|
5594
|
+
let lockResult;
|
|
5595
|
+
const lockPhase = async () => {
|
|
5596
|
+
lockResult = await this.lockArtifact(runId, artifact, false, agent);
|
|
5597
|
+
};
|
|
5598
|
+
const result = await auditToPass({
|
|
5599
|
+
teams,
|
|
5600
|
+
caller,
|
|
5601
|
+
runId,
|
|
5602
|
+
phase,
|
|
5603
|
+
blockedBy: input.blockedBy,
|
|
5604
|
+
writeScopes: input.writeScopes,
|
|
5605
|
+
reviewerName: input.reviewerName,
|
|
5606
|
+
maxRounds: input.maxRounds,
|
|
5607
|
+
waitTimeoutMs: input.waitTimeoutMs,
|
|
5608
|
+
runAuditRound,
|
|
5609
|
+
lockPhase
|
|
5610
|
+
});
|
|
5611
|
+
const report = {
|
|
5612
|
+
...result,
|
|
5613
|
+
history: renderTaskHistory(result.taskView, result.rounds)
|
|
5614
|
+
};
|
|
5615
|
+
if (lockResult !== void 0) report.lock = lockResult;
|
|
5616
|
+
return report;
|
|
5617
|
+
}
|
|
5618
|
+
/**
|
|
5619
|
+
* T1 (goals projection): project the run into the native goals service so it is
|
|
5620
|
+
* a first-class durable, resumable, blockable object. Best-effort — the run's
|
|
5621
|
+
* filesystem state is the source of truth; a goal is the durable projection.
|
|
5622
|
+
*/
|
|
5623
|
+
projectRunToGoal(agent, runId, state = "active") {
|
|
5624
|
+
if (!agent) return {
|
|
5625
|
+
ok: false,
|
|
5626
|
+
reason: "no agent"
|
|
5627
|
+
};
|
|
5628
|
+
return syncRunGoal(this.goalsService, agent, runId, state);
|
|
5629
|
+
}
|
|
5630
|
+
/** T1: block the run's goal on a gate-block (durable + UI-visible). */
|
|
5631
|
+
blockRunToGoal(agent, runId, reason) {
|
|
5632
|
+
if (!agent) return {
|
|
5633
|
+
ok: false,
|
|
5634
|
+
reason: "no agent"
|
|
5635
|
+
};
|
|
5636
|
+
return blockRunGoal(this.goalsService, agent, runId, reason);
|
|
5637
|
+
}
|
|
5638
|
+
/** T1: re-arm the run's goal on a reopen (blocked/paused -> active). */
|
|
5639
|
+
resumeRunToGoal(agent, runId) {
|
|
5640
|
+
if (!agent) return {
|
|
5641
|
+
ok: false,
|
|
5642
|
+
reason: "no agent"
|
|
5643
|
+
};
|
|
5644
|
+
return resumeRunGoal(this.goalsService, agent, runId);
|
|
5645
|
+
}
|
|
5646
|
+
/**
|
|
5070
5647
|
* Workspace-scoped control-plane root (R1 binding invariant).
|
|
5071
5648
|
* Resolves the session agent's canonical cwd -> workspace path via the
|
|
5072
5649
|
* registry; NEVER scans list(). Returns null when unavailable (defer).
|
|
@@ -5127,8 +5704,16 @@ var RecursiveRuntime = class extends Service {
|
|
|
5127
5704
|
/**
|
|
5128
5705
|
* Phase B (native delegation): build a review bundle (R1) + file-backed
|
|
5129
5706
|
* handoff docs (R2), resolve the role via the router policy (R3), and call
|
|
5130
|
-
* ctx.subagents
|
|
5131
|
-
*
|
|
5707
|
+
* ctx.subagents with the full request (R4). Workspace-scoped: every path
|
|
5708
|
+
* resolves under the session's control-plane root.
|
|
5709
|
+
*
|
|
5710
|
+
* `mode: 'continuable'` (T4) runs the audit→repair→re-audit loop on ONE
|
|
5711
|
+
* durable continuable child (startContinuable → followup with the repair
|
|
5712
|
+
* instruction → settle) and drains the child on closeout. It requires an
|
|
5713
|
+
* `awaitRoundResult` observer (the parent-side settlement seam) AND the exact
|
|
5714
|
+
* live `parent` Agent (continuable followup is object-identity authority);
|
|
5715
|
+
* when either is absent it falls back to one-shot `delegate()` with a flag —
|
|
5716
|
+
* never silently. One-shot `start()` is never called on the continuable path.
|
|
5132
5717
|
*/
|
|
5133
5718
|
async delegateReview(input) {
|
|
5134
5719
|
const policy = loadRouterPolicy(input.policyPath ?? routerPolicyPath(input.root));
|
|
@@ -5194,11 +5779,33 @@ var RecursiveRuntime = class extends Service {
|
|
|
5194
5779
|
toolFilter: input.toolFilter ?? defaultReviewToolFilter(),
|
|
5195
5780
|
maxDepth: input.maxDepth ?? 2
|
|
5196
5781
|
};
|
|
5782
|
+
if (input.parent !== void 0) request.parent = input.parent;
|
|
5197
5783
|
let result = null;
|
|
5198
5784
|
let error = null;
|
|
5785
|
+
let continuable = null;
|
|
5199
5786
|
if (decision.tier === "native" || decision.tier === "external-cli") {
|
|
5200
5787
|
if (!input.subagents) error = "no ctx.subagents runtime available (self-audit fallback)";
|
|
5201
|
-
else
|
|
5788
|
+
else if (input.mode === "continuable") {
|
|
5789
|
+
continuable = await delegateContinuable({
|
|
5790
|
+
subagents: input.subagents,
|
|
5791
|
+
provider: decision.provider,
|
|
5792
|
+
label: input.delegationId + "/" + input.childId,
|
|
5793
|
+
prompt,
|
|
5794
|
+
childId: input.childId,
|
|
5795
|
+
maxDepth: input.maxDepth ?? 2,
|
|
5796
|
+
toolFilter: input.toolFilter ?? defaultReviewToolFilter(),
|
|
5797
|
+
maxRounds: input.maxRounds ?? 3,
|
|
5798
|
+
awaitRoundResult: input.awaitRoundResult,
|
|
5799
|
+
parent: input.parent
|
|
5800
|
+
});
|
|
5801
|
+
if (continuable.fellBackToOneShot) {
|
|
5802
|
+
result = continuable.rounds[0]?.result ?? null;
|
|
5803
|
+
if (!result) error = "continuable fallback produced no result";
|
|
5804
|
+
} else if (continuable.ok && continuable.rounds.length > 0) {
|
|
5805
|
+
result = continuable.rounds[continuable.rounds.length - 1].result ?? null;
|
|
5806
|
+
if (!result) error = "continuable child produced no final result";
|
|
5807
|
+
} else error = continuable.reason ?? "continuable delegation failed";
|
|
5808
|
+
} else try {
|
|
5202
5809
|
result = await delegate({
|
|
5203
5810
|
subagents: input.subagents,
|
|
5204
5811
|
provider: decision.provider,
|
|
@@ -5218,7 +5825,7 @@ var RecursiveRuntime = class extends Service {
|
|
|
5218
5825
|
subagentId: input.childId,
|
|
5219
5826
|
phase: input.phase,
|
|
5220
5827
|
purpose: input.role + " for run " + input.runId,
|
|
5221
|
-
executionMode: decision.tier,
|
|
5828
|
+
executionMode: decision.tier + (input.mode === "continuable" ? " (continuable)" : ""),
|
|
5222
5829
|
artifactPath: input.artifactPath,
|
|
5223
5830
|
upstreamArtifacts: input.upstreamArtifacts,
|
|
5224
5831
|
reviewBundle: bundle.repoRelativePath,
|
|
@@ -5251,7 +5858,12 @@ var RecursiveRuntime = class extends Service {
|
|
|
5251
5858
|
result,
|
|
5252
5859
|
evaluation,
|
|
5253
5860
|
actionRecordPath,
|
|
5254
|
-
error
|
|
5861
|
+
error,
|
|
5862
|
+
continuable: continuable ? {
|
|
5863
|
+
rounds: continuable.rounds,
|
|
5864
|
+
childId: continuable.childId,
|
|
5865
|
+
fellBackToOneShot: continuable.fellBackToOneShot
|
|
5866
|
+
} : null
|
|
5255
5867
|
};
|
|
5256
5868
|
}
|
|
5257
5869
|
/** R6: validate a child's claimed references against actual files. */
|
|
@@ -5413,6 +6025,9 @@ var RecursiveRuntime = class extends Service {
|
|
|
5413
6025
|
existing
|
|
5414
6026
|
};
|
|
5415
6027
|
if (worktree) result.worktree = worktree;
|
|
6028
|
+
try {
|
|
6029
|
+
this.projectRunToGoal(agent, runId, "active");
|
|
6030
|
+
} catch {}
|
|
5416
6031
|
return result;
|
|
5417
6032
|
}
|
|
5418
6033
|
/**
|
|
@@ -5466,7 +6081,16 @@ var RecursiveRuntime = class extends Service {
|
|
|
5466
6081
|
if (!existsSync(artifactPath)) throw new Error("Artifact not found: " + artifact);
|
|
5467
6082
|
if (getLockStatus(artifactPath) === "LOCKED") throw new Error("Artifact already LOCKED: " + artifact);
|
|
5468
6083
|
const blockers = getPrerequisiteBlockers(runDir, artifact);
|
|
5469
|
-
if (blockers.length > 0)
|
|
6084
|
+
if (blockers.length > 0) {
|
|
6085
|
+
const message = "monotonic lock-order: " + blockers.map((b) => b.artifact + " (" + b.status + ")").join(", ");
|
|
6086
|
+
try {
|
|
6087
|
+
this.blockRunToGoal(agent, runId, {
|
|
6088
|
+
code: "prerequisite-blockers",
|
|
6089
|
+
message
|
|
6090
|
+
});
|
|
6091
|
+
} catch {}
|
|
6092
|
+
throw new Error("Prerequisite blockers: " + blockers.map((b) => b.artifact + " (" + b.status + ")").join(", "));
|
|
6093
|
+
}
|
|
5470
6094
|
let content = readFileSync(artifactPath, "utf8");
|
|
5471
6095
|
const lockedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
5472
6096
|
content = setOrInsertField(content, "Status", "LOCKED", ["Phase"]);
|
|
@@ -5495,6 +6119,9 @@ var RecursiveRuntime = class extends Service {
|
|
|
5495
6119
|
const hadReceipt = invalidateReceipt(runDir, artifact);
|
|
5496
6120
|
const stale = getStaleDownstreamPhases(runDir, artifact);
|
|
5497
6121
|
for (const entry of stale) invalidateReceipt(runDir, entry.artifact);
|
|
6122
|
+
try {
|
|
6123
|
+
this.resumeRunToGoal(agent, runId);
|
|
6124
|
+
} catch {}
|
|
5498
6125
|
return {
|
|
5499
6126
|
artifact,
|
|
5500
6127
|
runId,
|
|
@@ -5942,6 +6569,135 @@ function createRecursivePhaseTool(recursive) {
|
|
|
5942
6569
|
});
|
|
5943
6570
|
}
|
|
5944
6571
|
//#endregion
|
|
6572
|
+
//#region src/recursive_audit_team.tool.ts
|
|
6573
|
+
/**
|
|
6574
|
+
* `recursive_audit_team` — the T3 entry path: a turn-driven adapter over the
|
|
6575
|
+
* live `agentTeams` task board. ONE call advances the audit→repair→re-audit
|
|
6576
|
+
* state machine by one transition, so the agent drives the loop across turns
|
|
6577
|
+
* as continuable-child settlement notices arrive in its inbox.
|
|
6578
|
+
*
|
|
6579
|
+
* Why one step per call: the live subagents service has no public parent-side
|
|
6580
|
+
* "await settlement" promise — a continuable child's verdict arrives as a
|
|
6581
|
+
* `subagent-settled` message on a LATER turn. The pure whole-loop driver
|
|
6582
|
+
* (`auditToPass` in teams-loop.ts) models the full state machine and is the
|
|
6583
|
+
* tested reference; this tool is its honest turn-driven shell.
|
|
6584
|
+
*/
|
|
6585
|
+
/** Project one live task view to an owned, lossless-JSON-safe record. */
|
|
6586
|
+
function taskViewToJson(view) {
|
|
6587
|
+
return {
|
|
6588
|
+
id: view.id,
|
|
6589
|
+
revision: view.revision,
|
|
6590
|
+
subject: view.subject,
|
|
6591
|
+
description: view.description,
|
|
6592
|
+
status: view.status,
|
|
6593
|
+
blockedBy: [...view.blockedBy],
|
|
6594
|
+
writeScopes: [...view.writeScopes],
|
|
6595
|
+
ownerName: view.ownerName ?? null,
|
|
6596
|
+
ready: view.ready,
|
|
6597
|
+
writeScopeWarnings: [...view.writeScopeWarnings]
|
|
6598
|
+
};
|
|
6599
|
+
}
|
|
6600
|
+
/** Wrap a non-JSON-pure value in the standard error envelope. */
|
|
6601
|
+
function errorJson(message) {
|
|
6602
|
+
return { error: message };
|
|
6603
|
+
}
|
|
6604
|
+
function createRecursiveAuditTeamTool(teams) {
|
|
6605
|
+
return defineTool({
|
|
6606
|
+
name: "recursive_audit_team",
|
|
6607
|
+
description: "Advance one agentTeams Task-board transition for the recursive audit loop (create → claim → edit(REVISE) → complete(APPROVE) → release/interrupt). Drive one step per turn as continuable-child settlement notices arrive; complete the task (and lock the phase) ONLY after an APPROVE verdict.",
|
|
6608
|
+
parameters: {
|
|
6609
|
+
action: {
|
|
6610
|
+
type: "string",
|
|
6611
|
+
description: "create | claim | edit | complete | release | interrupt | get | list. Required."
|
|
6612
|
+
},
|
|
6613
|
+
taskId: {
|
|
6614
|
+
type: "string",
|
|
6615
|
+
description: "Task id for claim/edit/complete/release/interrupt/get."
|
|
6616
|
+
},
|
|
6617
|
+
expectedRevision: {
|
|
6618
|
+
type: "number",
|
|
6619
|
+
description: "CAS revision for claim/edit/complete/release."
|
|
6620
|
+
},
|
|
6621
|
+
subject: {
|
|
6622
|
+
type: "string",
|
|
6623
|
+
description: "Task subject (create)."
|
|
6624
|
+
},
|
|
6625
|
+
description: {
|
|
6626
|
+
type: "string",
|
|
6627
|
+
description: "Task description (create) or appended repair note (edit)."
|
|
6628
|
+
},
|
|
6629
|
+
blockedBy: {
|
|
6630
|
+
type: "array",
|
|
6631
|
+
items: { type: "string" },
|
|
6632
|
+
description: "Task blockers (create)."
|
|
6633
|
+
},
|
|
6634
|
+
writeScopes: {
|
|
6635
|
+
type: "array",
|
|
6636
|
+
items: { type: "string" },
|
|
6637
|
+
description: "Advisory write scopes (create)."
|
|
6638
|
+
},
|
|
6639
|
+
targetName: {
|
|
6640
|
+
type: "string",
|
|
6641
|
+
description: "Teammate name to interrupt (interrupt)."
|
|
6642
|
+
}
|
|
6643
|
+
},
|
|
6644
|
+
output: {
|
|
6645
|
+
schema: { type: "json" },
|
|
6646
|
+
render: (_args, value) => [{
|
|
6647
|
+
type: "text",
|
|
6648
|
+
text: JSON.stringify(value, null, 2)
|
|
6649
|
+
}]
|
|
6650
|
+
},
|
|
6651
|
+
async execute(args, exec) {
|
|
6652
|
+
const action = args.action ?? "";
|
|
6653
|
+
if (!teams) return errorJson("agentTeams service is not available in this composition");
|
|
6654
|
+
const caller = exec.agent ?? {};
|
|
6655
|
+
try {
|
|
6656
|
+
switch (action) {
|
|
6657
|
+
case "create": {
|
|
6658
|
+
if (!args.subject || !args.description) return errorJson("create requires subject and description");
|
|
6659
|
+
const createRequest = {
|
|
6660
|
+
subject: args.subject,
|
|
6661
|
+
description: args.description
|
|
6662
|
+
};
|
|
6663
|
+
if (args.blockedBy !== void 0) createRequest.blockedBy = args.blockedBy;
|
|
6664
|
+
if (args.writeScopes !== void 0) createRequest.writeScopes = args.writeScopes;
|
|
6665
|
+
return taskViewToJson(await teams.createTask(caller, createRequest));
|
|
6666
|
+
}
|
|
6667
|
+
case "claim":
|
|
6668
|
+
case "edit":
|
|
6669
|
+
case "complete":
|
|
6670
|
+
case "release": {
|
|
6671
|
+
if (!args.taskId || args.expectedRevision === void 0) return errorJson(action + " requires taskId and expectedRevision");
|
|
6672
|
+
const updateAction = action;
|
|
6673
|
+
const updateRequest = {
|
|
6674
|
+
taskId: args.taskId,
|
|
6675
|
+
expectedRevision: args.expectedRevision,
|
|
6676
|
+
action: updateAction
|
|
6677
|
+
};
|
|
6678
|
+
if (action === "edit" && args.description !== void 0) updateRequest.description = args.description;
|
|
6679
|
+
return taskViewToJson(await teams.updateTask(caller, updateRequest));
|
|
6680
|
+
}
|
|
6681
|
+
case "interrupt":
|
|
6682
|
+
if (!args.targetName) return errorJson("interrupt requires targetName");
|
|
6683
|
+
if (!teams.interrupt) return errorJson("no interrupt seam");
|
|
6684
|
+
return { previousStatus: teams.interrupt(caller, args.targetName).previousStatus };
|
|
6685
|
+
case "get":
|
|
6686
|
+
if (!args.taskId) return errorJson("get requires taskId");
|
|
6687
|
+
if (!teams.getTask) return errorJson("no getTask seam");
|
|
6688
|
+
return taskViewToJson(teams.getTask(caller, args.taskId));
|
|
6689
|
+
case "list":
|
|
6690
|
+
if (!teams.listTasks) return errorJson("no listTasks seam");
|
|
6691
|
+
return teams.listTasks(caller).map(taskViewToJson);
|
|
6692
|
+
default: return errorJson("unsupported action: " + action + " (create|claim|edit|complete|release|interrupt|get|list)");
|
|
6693
|
+
}
|
|
6694
|
+
} catch (err) {
|
|
6695
|
+
return errorJson(err instanceof Error ? err.message : String(err));
|
|
6696
|
+
}
|
|
6697
|
+
}
|
|
6698
|
+
});
|
|
6699
|
+
}
|
|
6700
|
+
//#endregion
|
|
5945
6701
|
//#region src/bootstrap.ts
|
|
5946
6702
|
/**
|
|
5947
6703
|
* Idempotent scaffold installer (R3). TS port of install-recursive-mode.py's
|
|
@@ -6891,12 +7647,15 @@ function apply(ctx, config) {
|
|
|
6891
7647
|
if (config?.shellOnly) return;
|
|
6892
7648
|
ctx.effect(function* () {
|
|
6893
7649
|
const workspaceRegistry = ctx.get("workspaceRegistry");
|
|
7650
|
+
const goals = ctx.get("goals");
|
|
6894
7651
|
const recursive = new RecursiveRuntime(ctx, {
|
|
6895
7652
|
repoRoot: config?.repoRoot ?? process.cwd(),
|
|
6896
|
-
workspaceRegistry
|
|
7653
|
+
workspaceRegistry,
|
|
7654
|
+
goals
|
|
6897
7655
|
});
|
|
6898
7656
|
const repairedRoots = /* @__PURE__ */ new Set();
|
|
6899
7657
|
const reminderGate = new ReminderOnceGate();
|
|
7658
|
+
const agentTeams = ctx.get("agentTeams");
|
|
6900
7659
|
const disposers = [
|
|
6901
7660
|
ctx.tools.register(createRecursiveStatusTool(recursive)),
|
|
6902
7661
|
ctx.tools.register(createRecursiveInitTool(recursive)),
|
|
@@ -6905,7 +7664,8 @@ function apply(ctx, config) {
|
|
|
6905
7664
|
ctx.tools.register(createRecursiveCloseoutTool(recursive)),
|
|
6906
7665
|
ctx.tools.register(createRecursiveScratchTool(recursive)),
|
|
6907
7666
|
ctx.tools.register(createRecursiveWorktreeTool(recursive)),
|
|
6908
|
-
ctx.tools.register(createRecursivePhaseTool(recursive))
|
|
7667
|
+
ctx.tools.register(createRecursivePhaseTool(recursive)),
|
|
7668
|
+
...agentTeams ? [ctx.tools.register(createRecursiveAuditTeamTool(agentTeams))] : []
|
|
6909
7669
|
];
|
|
6910
7670
|
const commands = ctx.get("commands");
|
|
6911
7671
|
if (commands) disposers.push(registerRecursiveCommand({ commands }, recursive));
|
|
@@ -6932,6 +7692,10 @@ function apply(ctx, config) {
|
|
|
6932
7692
|
const decision = evaluateToolGuard(exec, exec?.agent?.session?.header?.cwd ?? "", "", recursive.enforcementConfig.toolGuards);
|
|
6933
7693
|
if (decision.kind === "allow") return typeof next === "function" ? next() : { kind: "allow" };
|
|
6934
7694
|
if (decision.kind === "deny") return decision;
|
|
7695
|
+
const coerced = coerceAskToDecision(decision, recursive.enforcementConfig.toolGuards);
|
|
7696
|
+
if (coerced.kind === "deny") return coerced;
|
|
7697
|
+
if (coerced.kind === "allow" && coerced.warn) console.warn("[recursive] tool guard (advisory): " + coerced.warn + " — allowing");
|
|
7698
|
+
return typeof next === "function" ? next() : { kind: "allow" };
|
|
6935
7699
|
}));
|
|
6936
7700
|
const agentRuntime = ctx;
|
|
6937
7701
|
if (agentRuntime.on) disposers.push(agentRuntime.on("agent/pre-step", async (payload, next) => {
|
|
@@ -7011,4 +7775,4 @@ function apply(ctx, config) {
|
|
|
7011
7775
|
});
|
|
7012
7776
|
}
|
|
7013
7777
|
//#endregion
|
|
7014
|
-
export { DEFAULT_ENFORCEMENT, OPTIONAL_PHASES, PHASES, PHASE_SEQUENCE, RECURSIVE_API_PREFIX, RUN_ARTIFACT_SEQUENCE, RUN_STATES, RecursiveRuntime, apply, buildDelegationPrompt, buildReviewBundle, capabilityProbe, childScratchPath, contentSha256, coupleGateBlockToGoal, createChildBrief, createHandoff, createRecursiveCloseoutTool, createRecursiveInitTool, createRecursiveLintTool, createRecursiveLockTool, createRecursivePhaseTool, createRecursiveScratchTool, createRecursiveStatusTool, createRecursiveWorktreeTool, defaultReviewToolFilter, delegate, delegationDecisionBasis, delegationError, detectTamper, discoverRuns, escapeRegExp, evaluateDelegationResult, evaluateToolGuard, foldRun, foldRunCard, getAllStaleReceipts, getArtifactState, getGateStatus, getLatestRunDirectory, getLockStatus, getMdFieldValue, getNextLegalPhase, getPrerequisiteBlockers, getPrerequisites, getStaleDownstreamPhases, getTodoStats, getWorkflowProfile, inject, invalidateReceipt, isCoreArtifact, loadRouterPolicy, lockHashFromContent, makeRecursiveRoutes, mountRecursiveRoutesOnce, name, normalizeForLockHash, phaseIndex, probeCapabilities, readReceipt, receiptPath, renderRecursivePolicy, replyPath, resolveEnforcementConfig, resolveRole, resolveRunDir, reviewBundleDir, reviewOutputSchema, routerPolicyPath, snapshotWorkspace, trimMdValue, validateChain, validateReferences, validateTransition, writeActionRecord, writeReceipt };
|
|
7778
|
+
export { DEFAULT_ENFORCEMENT, OPTIONAL_PHASES, PHASES, PHASE_SEQUENCE, RECURSIVE_API_PREFIX, RUN_ARTIFACT_SEQUENCE, RUN_STATES, RecursiveRuntime, apply, auditToPass, buildDelegationPrompt, buildReviewBundle, capabilityProbe, childScratchPath, coerceAskToDecision, contentSha256, coupleGateBlockToGoal, createChildBrief, createHandoff, createRecursiveCloseoutTool, createRecursiveInitTool, createRecursiveLintTool, createRecursiveLockTool, createRecursivePhaseTool, createRecursiveScratchTool, createRecursiveStatusTool, createRecursiveWorktreeTool, defaultReviewToolFilter, delegate, delegateContinuable, delegationDecisionBasis, delegationError, detectTamper, discoverRuns, drainContinuableChildren, drainContinuableDescendants, escapeRegExp, evaluateDelegationResult, evaluateToolGuard, foldRun, foldRunCard, getAllStaleReceipts, getArtifactState, getGateStatus, getLatestRunDirectory, getLockStatus, getMdFieldValue, getNextLegalPhase, getPrerequisiteBlockers, getPrerequisites, getStaleDownstreamPhases, getTodoStats, getWorkflowProfile, inject, interruptContinuable, invalidateReceipt, isCoreArtifact, isTaskClaimedBy, loadRouterPolicy, lockHashFromContent, makeRecursiveRoutes, mountRecursiveRoutesOnce, name, normalizeForLockHash, phaseIndex, probeCapabilities, readReceipt, readRepairFromStructured, readVerdictFromStructured, receiptPath, renderRecursivePolicy, renderTaskHistory, replyPath, resolveEnforcementConfig, resolveRole, resolveRunDir, reviewBundleDir, reviewOutputSchema, routerPolicyPath, snapshotWorkspace, trimMdValue, validateChain, validateReferences, validateTransition, writeActionRecord, writeReceipt };
|