@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/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.start() with the full request (R4). Workspace-scoped: every
5131
- * path resolves under the session's control-plane root.
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 try {
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) throw new Error("Prerequisite blockers: " + blockers.map((b) => b.artifact + " (" + b.status + ")").join(", "));
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
@@ -5956,12 +6712,12 @@ function createRecursivePhaseTool(recursive) {
5956
6712
  * references/ (never inlined TS string literals) and resolved relative to this
5957
6713
  * module (package install location), never process.cwd().
5958
6714
  */
5959
- const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
6715
+ const MODULE_DIR$1 = dirname(fileURLToPath(import.meta.url));
5960
6716
  /** Package root: <package>/src/.. — references/ sits next to src/. */
5961
- const PACKAGE_ROOT = join(MODULE_DIR, "..");
6717
+ const PACKAGE_ROOT$1 = join(MODULE_DIR$1, "..");
5962
6718
  /** references/ root (shipped package files). */
5963
6719
  function referencesRoot() {
5964
- return join(PACKAGE_ROOT, "references");
6720
+ return join(PACKAGE_ROOT$1, "references");
5965
6721
  }
5966
6722
  const M = {
5967
6723
  recursiveStart: "<!-- RECURSIVE-MODE-CANONICAL:START -->",
@@ -6870,6 +7626,91 @@ function mountRecursiveRoutesOnce(packageName, makeRoutes, webServer) {
6870
7626
  };
6871
7627
  }
6872
7628
  //#endregion
7629
+ //#region src/skills.ts
7630
+ /**
7631
+ * Packaged `recursive-mode` skill (dsh plugin standard).
7632
+ *
7633
+ * Ships the workflow's operating contract as a bundled skill through
7634
+ * `ctx.skills.registerProvider(...)` — the same shape as the shipped
7635
+ * `dsh-skill-badge` provider: a `bundled` candidate at `BUNDLED_SKILL_RANK`
7636
+ * (600), body read from the package's shipped `skills/recursive-mode/SKILL.md`
7637
+ * (never an inlined TS string literal), with a directory resource base so the
7638
+ * skill can resolve its own assets. Skills are optional instructions, not
7639
+ * session events: this emits nothing and never appends a recursive/* event.
7640
+ *
7641
+ * Optionality: `skills` is a host-plane registry; when the composition has
7642
+ * none, this is a no-op (returns undefined) rather than failing boot.
7643
+ */
7644
+ /** Package root: <package>/src/.. — skills/ sits next to src/. */
7645
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
7646
+ const PACKAGE_ROOT = join(MODULE_DIR, "..");
7647
+ /** Precedence rank for packaged/bundled skills (mirrors `BUNDLED_SKILL_RANK`). */
7648
+ const BUNDLED_SKILL_RANK = 600;
7649
+ const SKILL_NAME = "recursive-mode";
7650
+ const PROVIDER_NAME = "recursive-mode";
7651
+ const SKILL_BODY_PATH = join(PACKAGE_ROOT, "skills", SKILL_NAME, "SKILL.md");
7652
+ const SKILL_RESOURCE_BASE = join(PACKAGE_ROOT, "skills", SKILL_NAME);
7653
+ const SKILL_DESCRIPTION = "Drive the recursive-mode workflow: an audit-gated, phase-disciplined loop that carries one requirement from AS-IS through TO-BE plan, implementation, test, and manual QA to locked control-plane state and durable memory. Use whenever the user asks to implement, resume, or advance a recursive run, or when a repo carries a /.recursive/ control plane.";
7654
+ /** Invocation policy: both the model catalog and user `/name` surfaces may load it. */
7655
+ const INVOCATION = {
7656
+ modelInvocable: true,
7657
+ userInvocable: true
7658
+ };
7659
+ /** The bundled candidate, stable across every `list()` call. */
7660
+ function candidate() {
7661
+ return {
7662
+ name: SKILL_NAME,
7663
+ description: SKILL_DESCRIPTION,
7664
+ invocation: INVOCATION,
7665
+ provider: PROVIDER_NAME,
7666
+ source: "bundled",
7667
+ resourceBase: {
7668
+ kind: "directory",
7669
+ path: SKILL_RESOURCE_BASE
7670
+ },
7671
+ rank: BUNDLED_SKILL_RANK,
7672
+ locator: SKILL_BODY_PATH,
7673
+ path: SKILL_BODY_PATH
7674
+ };
7675
+ }
7676
+ /** Read the shipped operating-contract body (fail loud if the package lost it). */
7677
+ function readBody() {
7678
+ return readFileSync(SKILL_BODY_PATH, "utf8").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
7679
+ }
7680
+ /** The one bundled provider this plugin contributes. */
7681
+ function provider() {
7682
+ return {
7683
+ name: PROVIDER_NAME,
7684
+ list: () => Promise.resolve([candidate()]),
7685
+ get: async () => ({
7686
+ name: SKILL_NAME,
7687
+ description: SKILL_DESCRIPTION,
7688
+ invocation: INVOCATION,
7689
+ provider: PROVIDER_NAME,
7690
+ source: "bundled",
7691
+ resourceBase: {
7692
+ kind: "directory",
7693
+ path: SKILL_RESOURCE_BASE
7694
+ },
7695
+ content: readBody(),
7696
+ path: SKILL_BODY_PATH
7697
+ })
7698
+ };
7699
+ }
7700
+ /**
7701
+ * Register the packaged `recursive-mode` skill into the host `skills` registry.
7702
+ *
7703
+ * Reads `ctx.get('skills')` optionally (the registry is host-plane; a
7704
+ * composition without it is valid). On success returns the exact disposer that
7705
+ * unregisters the provider; on absence returns undefined (a no-op, never a boot
7706
+ * failure).
7707
+ */
7708
+ function registerRecursiveSkill(ctx) {
7709
+ const skills = ctx.get("skills");
7710
+ if (skills === void 0 || skills === null) return void 0;
7711
+ return skills.registerProvider(() => provider());
7712
+ }
7713
+ //#endregion
6873
7714
  //#region src/index.ts
6874
7715
  const name = "@try-works/dsh-recursive-mode";
6875
7716
  /**
@@ -6891,13 +7732,18 @@ function apply(ctx, config) {
6891
7732
  if (config?.shellOnly) return;
6892
7733
  ctx.effect(function* () {
6893
7734
  const workspaceRegistry = ctx.get("workspaceRegistry");
7735
+ const goals = ctx.get("goals");
6894
7736
  const recursive = new RecursiveRuntime(ctx, {
6895
7737
  repoRoot: config?.repoRoot ?? process.cwd(),
6896
- workspaceRegistry
7738
+ workspaceRegistry,
7739
+ goals
6897
7740
  });
6898
7741
  const repairedRoots = /* @__PURE__ */ new Set();
6899
7742
  const reminderGate = new ReminderOnceGate();
7743
+ const agentTeams = ctx.get("agentTeams");
7744
+ const skillDisposer = registerRecursiveSkill(ctx);
6900
7745
  const disposers = [
7746
+ ...skillDisposer ? [skillDisposer] : [],
6901
7747
  ctx.tools.register(createRecursiveStatusTool(recursive)),
6902
7748
  ctx.tools.register(createRecursiveInitTool(recursive)),
6903
7749
  ctx.tools.register(createRecursiveLockTool(recursive)),
@@ -6905,7 +7751,8 @@ function apply(ctx, config) {
6905
7751
  ctx.tools.register(createRecursiveCloseoutTool(recursive)),
6906
7752
  ctx.tools.register(createRecursiveScratchTool(recursive)),
6907
7753
  ctx.tools.register(createRecursiveWorktreeTool(recursive)),
6908
- ctx.tools.register(createRecursivePhaseTool(recursive))
7754
+ ctx.tools.register(createRecursivePhaseTool(recursive)),
7755
+ ...agentTeams ? [ctx.tools.register(createRecursiveAuditTeamTool(agentTeams))] : []
6909
7756
  ];
6910
7757
  const commands = ctx.get("commands");
6911
7758
  if (commands) disposers.push(registerRecursiveCommand({ commands }, recursive));
@@ -6932,6 +7779,10 @@ function apply(ctx, config) {
6932
7779
  const decision = evaluateToolGuard(exec, exec?.agent?.session?.header?.cwd ?? "", "", recursive.enforcementConfig.toolGuards);
6933
7780
  if (decision.kind === "allow") return typeof next === "function" ? next() : { kind: "allow" };
6934
7781
  if (decision.kind === "deny") return decision;
7782
+ const coerced = coerceAskToDecision(decision, recursive.enforcementConfig.toolGuards);
7783
+ if (coerced.kind === "deny") return coerced;
7784
+ if (coerced.kind === "allow" && coerced.warn) console.warn("[recursive] tool guard (advisory): " + coerced.warn + " — allowing");
7785
+ return typeof next === "function" ? next() : { kind: "allow" };
6935
7786
  }));
6936
7787
  const agentRuntime = ctx;
6937
7788
  if (agentRuntime.on) disposers.push(agentRuntime.on("agent/pre-step", async (payload, next) => {
@@ -7011,4 +7862,4 @@ function apply(ctx, config) {
7011
7862
  });
7012
7863
  }
7013
7864
  //#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 };
7865
+ 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, registerRecursiveSkill, renderRecursivePolicy, renderTaskHistory, replyPath, resolveEnforcementConfig, resolveRole, resolveRunDir, reviewBundleDir, reviewOutputSchema, routerPolicyPath, snapshotWorkspace, trimMdValue, validateChain, validateReferences, validateTransition, writeActionRecord, writeReceipt };