audit-tools 0.32.7 → 0.32.9

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.
Files changed (128) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +2 -0
  3. package/dist/audit/cli/dispatch/quotaPool.d.ts +20 -2
  4. package/dist/audit/cli/dispatch/quotaPool.d.ts.map +1 -1
  5. package/dist/audit/cli/dispatch/quotaPool.js +38 -7
  6. package/dist/audit/cli/dispatch/quotaPool.js.map +1 -1
  7. package/dist/audit/cli/dispatch/tierRouting.d.ts +4 -1
  8. package/dist/audit/cli/dispatch/tierRouting.d.ts.map +1 -1
  9. package/dist/audit/cli/dispatch/tierRouting.js +7 -4
  10. package/dist/audit/cli/dispatch/tierRouting.js.map +1 -1
  11. package/dist/audit/cli/dispatch/types.d.ts +9 -4
  12. package/dist/audit/cli/dispatch/types.d.ts.map +1 -1
  13. package/dist/audit/cli/dispatch.d.ts +7 -0
  14. package/dist/audit/cli/dispatch.d.ts.map +1 -1
  15. package/dist/audit/cli/dispatch.js +24 -12
  16. package/dist/audit/cli/dispatch.js.map +1 -1
  17. package/dist/audit/cli/mergeAndIngestCommand.d.ts +0 -9
  18. package/dist/audit/cli/mergeAndIngestCommand.d.ts.map +1 -1
  19. package/dist/audit/cli/mergeAndIngestCommand.js +29 -1
  20. package/dist/audit/cli/mergeAndIngestCommand.js.map +1 -1
  21. package/dist/audit/cli/prompts.d.ts.map +1 -1
  22. package/dist/audit/cli/prompts.js +4 -2
  23. package/dist/audit/cli/prompts.js.map +1 -1
  24. package/dist/audit/cli/rollingAuditDispatch.d.ts.map +1 -1
  25. package/dist/audit/cli/rollingAuditDispatch.js +3 -0
  26. package/dist/audit/cli/rollingAuditDispatch.js.map +1 -1
  27. package/dist/audit/cli/semanticReviewStep.d.ts.map +1 -1
  28. package/dist/audit/cli/semanticReviewStep.js +10 -6
  29. package/dist/audit/cli/semanticReviewStep.js.map +1 -1
  30. package/dist/audit/cli/steps.d.ts +20 -10
  31. package/dist/audit/cli/steps.d.ts.map +1 -1
  32. package/dist/audit/cli/steps.js +4 -2
  33. package/dist/audit/cli/steps.js.map +1 -1
  34. package/dist/audit/orchestrator/planningExecutors.d.ts.map +1 -1
  35. package/dist/audit/orchestrator/planningExecutors.js +1 -0
  36. package/dist/audit/orchestrator/planningExecutors.js.map +1 -1
  37. package/dist/audit/orchestrator/rollingDispatch.d.ts +7 -6
  38. package/dist/audit/orchestrator/rollingDispatch.d.ts.map +1 -1
  39. package/dist/audit/orchestrator/rollingDispatch.js +37 -19
  40. package/dist/audit/orchestrator/rollingDispatch.js.map +1 -1
  41. package/dist/audit/quota/index.d.ts +137 -8
  42. package/dist/audit/quota/index.d.ts.map +1 -1
  43. package/dist/audit/quota/index.js +9 -8
  44. package/dist/audit/quota/index.js.map +1 -1
  45. package/dist/remediate/contractPipeline/derive.d.ts +20 -0
  46. package/dist/remediate/contractPipeline/derive.d.ts.map +1 -1
  47. package/dist/remediate/contractPipeline/derive.js +91 -0
  48. package/dist/remediate/contractPipeline/derive.js.map +1 -1
  49. package/dist/remediate/dispatch/ownershipRegistry.d.ts +2 -22
  50. package/dist/remediate/dispatch/ownershipRegistry.d.ts.map +1 -1
  51. package/dist/remediate/dispatch/ownershipRegistry.js +7 -45
  52. package/dist/remediate/dispatch/ownershipRegistry.js.map +1 -1
  53. package/dist/remediate/index.d.ts.map +1 -1
  54. package/dist/remediate/index.js +17 -1
  55. package/dist/remediate/index.js.map +1 -1
  56. package/dist/remediate/phases/close.d.ts.map +1 -1
  57. package/dist/remediate/phases/close.js +3 -2
  58. package/dist/remediate/phases/close.js.map +1 -1
  59. package/dist/remediate/riskSignal.d.ts +6 -3
  60. package/dist/remediate/riskSignal.d.ts.map +1 -1
  61. package/dist/remediate/riskSignal.js +6 -3
  62. package/dist/remediate/riskSignal.js.map +1 -1
  63. package/dist/remediate/steps/contractPipeline.d.ts +1 -2
  64. package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
  65. package/dist/remediate/steps/contractPipeline.js +80 -106
  66. package/dist/remediate/steps/contractPipeline.js.map +1 -1
  67. package/dist/remediate/steps/dispatch.d.ts +22 -1
  68. package/dist/remediate/steps/dispatch.d.ts.map +1 -1
  69. package/dist/remediate/steps/dispatch.js +108 -5
  70. package/dist/remediate/steps/dispatch.js.map +1 -1
  71. package/dist/remediate/steps/nextStep.d.ts.map +1 -1
  72. package/dist/remediate/steps/nextStep.js +89 -116
  73. package/dist/remediate/steps/nextStep.js.map +1 -1
  74. package/dist/remediate/steps/rollingSession.d.ts +82 -17
  75. package/dist/remediate/steps/rollingSession.d.ts.map +1 -1
  76. package/dist/remediate/steps/rollingSession.js +157 -58
  77. package/dist/remediate/steps/rollingSession.js.map +1 -1
  78. package/dist/remediate/steps/types.d.ts +12 -4
  79. package/dist/remediate/steps/types.d.ts.map +1 -1
  80. package/dist/remediate/steps/types.js +1 -1
  81. package/dist/shared/dispatch/admissionLoop.d.ts +245 -0
  82. package/dist/shared/dispatch/admissionLoop.d.ts.map +1 -0
  83. package/dist/shared/dispatch/admissionLoop.js +204 -0
  84. package/dist/shared/dispatch/admissionLoop.js.map +1 -0
  85. package/dist/{remediate → shared}/dispatch/ownershipScheduler.d.ts +23 -2
  86. package/dist/shared/dispatch/ownershipScheduler.d.ts.map +1 -0
  87. package/dist/{remediate → shared}/dispatch/ownershipScheduler.js +34 -6
  88. package/dist/shared/dispatch/ownershipScheduler.js.map +1 -0
  89. package/dist/shared/dispatch/pathIdentity.d.ts +26 -0
  90. package/dist/shared/dispatch/pathIdentity.d.ts.map +1 -0
  91. package/dist/shared/dispatch/pathIdentity.js +49 -0
  92. package/dist/shared/dispatch/pathIdentity.js.map +1 -0
  93. package/dist/shared/dispatch/rollingDispatch.d.ts +37 -0
  94. package/dist/shared/dispatch/rollingDispatch.d.ts.map +1 -1
  95. package/dist/shared/dispatch/rollingDispatch.js +117 -18
  96. package/dist/shared/dispatch/rollingDispatch.js.map +1 -1
  97. package/dist/shared/dispatch/unifiedRolling.d.ts +131 -0
  98. package/dist/shared/dispatch/unifiedRolling.d.ts.map +1 -0
  99. package/dist/shared/dispatch/unifiedRolling.js +161 -0
  100. package/dist/shared/dispatch/unifiedRolling.js.map +1 -0
  101. package/dist/shared/friction/stepBoundaryCapture.d.ts +4 -1
  102. package/dist/shared/friction/stepBoundaryCapture.d.ts.map +1 -1
  103. package/dist/shared/friction/stepBoundaryCapture.js.map +1 -1
  104. package/dist/shared/index.d.ts +12 -1
  105. package/dist/shared/index.d.ts.map +1 -1
  106. package/dist/shared/index.js +18 -1
  107. package/dist/shared/index.js.map +1 -1
  108. package/dist/shared/quota/packetCost.d.ts +29 -0
  109. package/dist/shared/quota/packetCost.d.ts.map +1 -0
  110. package/dist/shared/quota/packetCost.js +51 -0
  111. package/dist/shared/quota/packetCost.js.map +1 -0
  112. package/dist/shared/quota/reservationLedger.d.ts +100 -0
  113. package/dist/shared/quota/reservationLedger.d.ts.map +1 -0
  114. package/dist/shared/quota/reservationLedger.js +224 -0
  115. package/dist/shared/quota/reservationLedger.js.map +1 -0
  116. package/dist/shared/quota/state.d.ts +20 -0
  117. package/dist/shared/quota/state.d.ts.map +1 -1
  118. package/dist/shared/quota/state.js +50 -0
  119. package/dist/shared/quota/state.js.map +1 -1
  120. package/dist/shared/quota/types.d.ts +11 -0
  121. package/dist/shared/quota/types.d.ts.map +1 -1
  122. package/dist/shared/quota/types.js.map +1 -1
  123. package/docs/audit-pkg/contracts.md +3 -1
  124. package/docs/audit-pkg/development.md +5 -4
  125. package/docs/audit-pkg/release.md +2 -3
  126. package/package.json +3 -1
  127. package/dist/remediate/dispatch/ownershipScheduler.d.ts.map +0 -1
  128. package/dist/remediate/dispatch/ownershipScheduler.js.map +0 -1
@@ -2,7 +2,7 @@ import { existsSync, statSync } from "node:fs";
2
2
  import { mkdir, readFile, rename } from "node:fs/promises";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { StateStore } from "../state/store.js";
5
- import { readOptionalJsonFile, writeJsonFile, writeTextFile, buildAuditDeliverablePair, formatValidationIssues, isRecord, withFsRetry, RunLogger, DISPATCH_PROMPT_HANDOFF_NOTE, renderQuotaCoverageNudge, renderTokenBudgetView, coerceJsonObjectArg, createRollingDispatcher, setQuotaStateDir, interpretFreeFormIntent, advance, decideFrictionTriage, buildFrictionTriageBlock, planHybridDispatch, readSettledPools, addSettledPool, sourceByPoolId, classifyProvider, selectDispatchDriver, renderDispatchDriverInstruction, HostSessionQuotaSource, buildProviderModelKey, captureStepBoundaryFriction, LENSES, SEVERITIES } from "audit-tools/shared";
5
+ import { readOptionalJsonFile, writeJsonFile, writeTextFile, buildAuditDeliverablePair, formatValidationIssues, isRecord, withFsRetry, RunLogger, DISPATCH_PROMPT_HANDOFF_NOTE, renderQuotaCoverageNudge, renderTokenBudgetView, coerceJsonObjectArg, driveRolling, resolveLedgerBudgets, setQuotaStateDir, interpretFreeFormIntent, advance, decideFrictionTriage, buildFrictionTriageBlock, planHybridDispatch, readSettledPools, addSettledPool, sourceByPoolId, classifyProvider, selectDispatchDriver, renderDispatchDriverInstruction, HostSessionQuotaSource, buildProviderModelKey, captureStepBoundaryFriction, LENSES, SEVERITIES } from "audit-tools/shared";
6
6
  import { applyPlanPipeline, buildCoverageLedger } from "../phases/plan.js";
7
7
  import { groundExtractedFindings } from "../phases/grounding.js";
8
8
  import { runTriagePhase } from "../phases/triage.js";
@@ -10,7 +10,6 @@ import { runClosePhase } from "../phases/close.js";
10
10
  import { validateRemediationPlan } from "../validation/remediationState.js";
11
11
  import { mergeImplementResults, prepareImplementDispatch, readExtractedPlanIfPresent, buildConfirmedPools, executeNodeInWorktree, blockScopesFromPlan, declaredPathsFromPlan, targetedCommandsForBlock, } from "./dispatch.js";
12
12
  import { makeProviderNodeDispatcher } from "./providerNodeDispatch.js";
13
- import { ownershipSubWaves } from "../dispatch/ownershipScheduler.js";
14
13
  import { prepareHostRollingDispatch, nodeClaimRegistry, nodeSettledPoolsPath } from "./rollingSession.js";
15
14
  import { ClaimRegistry } from "../../shared/quota/claimRegistry.js";
16
15
  import { claimWithBackoff, withClaimHeartbeat } from "../../shared/quota/claimLease.js";
@@ -339,9 +338,9 @@ function blockedByUnsatisfiedDependency(state) {
339
338
  // The rolling scheduler replaced the wave-batch shim: instead of dispatching one
340
339
  // fixed-size wave per next-step and folding back through `implementing`, a node
341
340
  // becomes eligible the instant every dependency reaches a verified-complete
342
- // disposition (INV-RS-01). Concurrency is owned entirely by the quota scheduler
343
- // (the `dispatch-quota.json` max_concurrent_agents from `scheduleWave` /
344
- // `computeDispatchCapacity` — INV-S05 / INV-QD-11), never a separate wave cap.
341
+ // disposition (INV-RS-01). Concurrency is emergent from admission control (the
342
+ // `dispatch-quota.json` `admission.granted_packet_ids` the tool admits against the
343
+ // live budget — INV-S05 / INV-QD-11), never a computed wave cap.
345
344
  // A shared rebuild is interposed between dependency levels so a downstream node
346
345
  // typechecks/runs against the freshly-built upstream `audit-tools/shared`
347
346
  // surface; the rebuild is single-flight (CE-001) so the same package is never
@@ -495,103 +494,69 @@ export async function driveRollingDispatch(levels, options) {
495
494
  setQuotaStateDir(options.quotaStateDir);
496
495
  }
497
496
  const estimateTokens = options.estimateTokens ?? (() => 2000);
498
- const out = { levels: [], rebuilds: 0 };
499
- let rebuildInFlight = false; // single-flight guard (CE-001)
500
- for (let levelIndex = 0; levelIndex < levels.length; levelIndex++) {
501
- const level = levels[levelIndex];
502
- // Interpose the shared rebuild BEFORE every level after the first. Guarded so
503
- // it can never run twice or concurrently for the same boundary.
504
- if (levelIndex > 0) {
505
- if (rebuildInFlight) {
506
- throw new Error("driveRollingDispatch: shared rebuild already in flightsingle-flight invariant violated (CE-001).");
507
- }
508
- rebuildInFlight = true;
509
- try {
510
- await options.rebuildSharedBetweenLevels();
511
- out.rebuilds += 1;
512
- }
513
- finally {
514
- rebuildInFlight = false;
497
+ const scopeForBlock = options.scopeForBlock ?? ((b) => b.touched_files ?? []);
498
+ const allBlocks = levels.flat();
499
+ const blockById = new Map(allBlocks.map((b) => [b.block_id, b]));
500
+ const hostSession = options.hostSession;
501
+ // Admission control: when a metered pool reports a finite absolute budget, the shared
502
+ // reservation ledger leases each node's cost before dispatch, so co-located dispatch
503
+ // loops on one account (a second IDE, the host + its subagents) never collectively
504
+ // over-admit. On the claude-code host (percent-only quota, no finite ceiling) the ledger
505
+ // is omitted and the reactive 429 floor is the safety no per-dispatch lock overhead.
506
+ const ledgerCfg = resolveLedgerBudgets({
507
+ pools: options.confirmedPools,
508
+ sessionConfig: options.sessionConfig,
509
+ pendingItemTokens: allBlocks.map((b) => estimateTokens(b)),
510
+ });
511
+ // The remediate terminal adapter over the unified shared driver. What stays here is
512
+ // remediate's projection: a block → its ownership node (declared write-scope, so the
513
+ // shared `ownershipSubWaves` serializes same-file nodes / parallelizes disjoint ones)
514
+ // and → its dispatch packet; the shared rebuild between dependency levels; and the
515
+ // host-session escalation wiring. The loop, sub-wave split, and quota_paused/empty_pool
516
+ // terminal merge are the unified driver's.
517
+ const run = await driveRolling({
518
+ levels,
519
+ confirmedPools: options.confirmedPools,
520
+ sessionConfig: options.sessionConfig,
521
+ ...(ledgerCfg.reservationLedger
522
+ ? {
523
+ reservationLedger: ledgerCfg.reservationLedger,
524
+ resolvePoolBudget: ledgerCfg.resolvePoolBudget,
525
+ resolveOutputReservation: (_packet, poolId) => ledgerCfg.resolveOutputReservation(poolId),
515
526
  }
516
- }
517
- const blockByPacketId = new Map(level.map((b) => [b.block_id, b]));
518
- const scopeForBlock = options.scopeForBlock ?? ((b) => b.touched_files ?? []);
519
- // File-ownership-disjoint admission (INV-SOO): split the level into ordered
520
- // sub-waves, each a maximal file-disjoint subset (deterministic block_id
521
- // tie-break AFTER the disjointness filter, INV-SOO-08). Same-file nodes land
522
- // in successive sub-waves (serialize, INV-SOO-01/02) and a freed file becomes
523
- // schedulable for the next sub-wave; different-file nodes share a sub-wave and
524
- // parallelize up to the quota cap the dispatcher enforces (INV-SOO-03/05).
525
- const subWaves = ownershipSubWaves(level.map((b) => ({
527
+ : {}),
528
+ toNode: (b) => ({
526
529
  block_id: b.block_id,
527
530
  write_paths: scopeForBlock(b),
528
531
  ...(b.cofile_parallel_safe !== undefined
529
532
  ? { cofile_parallel_safe: b.cofile_parallel_safe }
530
533
  : {}),
531
- })), options.root);
532
- const levelResults = [];
533
- for (const wave of subWaves) {
534
- const packets = wave.map((node) => ({
535
- id: node.block_id,
536
- payload: { block_id: node.block_id },
537
- estimatedTokens: estimateTokens(blockByPacketId.get(node.block_id)),
538
- complexity: 0.5,
539
- }));
540
- const hostSession = options.hostSession;
541
- const dispatcher = createRollingDispatcher({
542
- confirmedPools: options.confirmedPools,
543
- sessionConfig: options.sessionConfig,
544
- dispatchPacket: async (packet, slot) => {
545
- const block = blockByPacketId.get(packet.payload.block_id);
546
- return options.dispatchNode(block, slot);
547
- },
548
- // Host-session escalation: feed recordLimit (write) at the rate_limited
549
- // observation point and read isEscalated (strand-not-requeue). The SAME
550
- // instance sized the pools, so the bounded re-limit count is account-wide.
551
- ...(hostSession
552
- ? {
553
- recordRateLimit: (packet, result) => hostSession.recordLimit(result.rateLimit?.channel ?? "error", result.rateLimit?.text ?? "", packet.id),
554
- isPacketEscalated: (packetId) => hostSession.isEscalated(packetId),
555
- }
556
- : {}),
557
- });
558
- dispatcher.enqueue(packets);
559
- levelResults.push(...(await dispatcher.run()));
560
- // Piece D: surface the wave's partial-completion terminal (quota_paused /
561
- // empty_pool). Aggregate across waves keeping the EARLIEST-reset quota_paused
562
- // terminal (a paused wave is retryable and should resume soonest); union the
563
- // stranded ids so a later step re-dispatches all of them. An empty_pool
564
- // terminal only stands when no quota_paused terminal was seen.
565
- out.terminal = mergePartialTerminals(out.terminal, dispatcher.getTerminal());
566
- }
567
- out.levels.push({ blockIds: level.map((b) => b.block_id), results: levelResults });
568
- }
569
- return out;
570
- }
571
- /**
572
- * Fold a newly-observed partial-completion terminal into the run's aggregate
573
- * (piece D). Prefers `quota_paused` (retryable) over `empty_pool`; among
574
- * quota_paused terminals keeps the EARLIEST `earliest_reset_at`; always unions the
575
- * stranded ids so no stranded node is lost across waves.
576
- */
577
- function mergePartialTerminals(prior, next) {
578
- if (!next)
579
- return prior;
580
- if (!prior)
581
- return next;
582
- const strandedIds = [...new Set([...prior.stranded_ids, ...next.stranded_ids])];
583
- const priorPaused = prior.reason === "quota_paused";
584
- const nextPaused = next.reason === "quota_paused";
585
- if (priorPaused || nextPaused) {
586
- const resets = [prior.earliest_reset_at, next.earliest_reset_at].filter((r) => typeof r === "string");
587
- const earliest = resets.length > 0 ? resets.sort()[0] : undefined;
588
- return {
589
- reason: "quota_paused",
590
- stranded_ids: strandedIds,
591
- ...(earliest ? { earliest_reset_at: earliest } : {}),
592
- };
593
- }
594
- return { reason: prior.reason, stranded_ids: strandedIds };
534
+ }),
535
+ toPacket: (b) => ({
536
+ id: b.block_id,
537
+ payload: { block_id: b.block_id },
538
+ estimatedTokens: estimateTokens(b),
539
+ complexity: 0.5,
540
+ }),
541
+ dispatchPacket: async (packet, slot) => options.dispatchNode(blockById.get(packet.payload.block_id), slot),
542
+ ...(options.root !== undefined ? { root: options.root } : {}),
543
+ // Single-flight (CE-001) is enforced by the unified driver.
544
+ rebuildBetweenLevels: options.rebuildSharedBetweenLevels,
545
+ // Host-session escalation: feed recordLimit (write) at the rate_limited observation
546
+ // point and read isEscalated (strand-not-requeue). The SAME instance sized the pools,
547
+ // so the bounded re-limit count is account-wide.
548
+ ...(hostSession
549
+ ? {
550
+ recordRateLimit: (packet, result) => hostSession.recordLimit(result.rateLimit?.channel ?? "error", result.rateLimit?.text ?? "", packet.id),
551
+ isPacketEscalated: (packetId) => hostSession.isEscalated(packetId),
552
+ }
553
+ : {}),
554
+ });
555
+ return {
556
+ levels: run.levels.map((l) => ({ blockIds: l.nodeIds, results: l.results })),
557
+ rebuilds: run.rebuilds,
558
+ ...(run.terminal ? { terminal: run.terminal } : {}),
559
+ };
595
560
  }
596
561
  /**
597
562
  * Backends the orchestrator can drive IN-PROCESS as the per-node implement worker
@@ -665,6 +630,10 @@ export async function driveRollingImplementDispatch(options) {
665
630
  hostModelId: options.waveOptions?.hostModelId,
666
631
  // Each node runs in its own worktree, so its prompt is rooted there.
667
632
  worktreeRootedPrompts: true,
633
+ // The in-process rolling engine admits + leases per-packet itself, so the
634
+ // dispatch-quota grant here must NOT lease (a host grant lease would
635
+ // double-count the same work against the shared account budget).
636
+ grantLeases: false,
668
637
  });
669
638
  if (plan.items.length === 0) {
670
639
  return null;
@@ -1366,7 +1335,10 @@ async function buildImplementDispatchStep(ctx) {
1366
1335
  const driverSelection = selectDispatchDriver({
1367
1336
  classification: classifyProvider(hostProvider),
1368
1337
  eligibleItemCount: rolling.session.frontier.length,
1369
- slots: rolling.session.slots,
1338
+ // The granted set's size IS the instantaneous admission width — there is no
1339
+ // separate concurrency number. The whole granted set runs at once, so the
1340
+ // driver-selection "slots" is the granted-set size.
1341
+ slots: rolling.session.frontier.length,
1370
1342
  });
1371
1343
  const rollMerge = loaderCommand(`merge-implement-results --run-id ${runId}`);
1372
1344
  const rollNext = loaderCommand("next-step");
@@ -1383,21 +1355,23 @@ async function buildImplementDispatchStep(ctx) {
1383
1355
  prompt: `
1384
1356
  # Dispatch Implementation Work (host-subagent rolling, worktree-isolated)
1385
1357
 
1386
- Each eligible node runs in its OWN git worktree (hard isolation between nodes). The
1358
+ Each granted node runs in its OWN git worktree (hard isolation between nodes). The
1387
1359
  TOOL owns commit -> verify -> merge + write-scope; you only spawn a subagent per
1388
1360
  node and call \`accept-node\` as each finishes.
1389
1361
 
1390
- Concurrency target: **${rolling.session.slots}** subagents at once (the quota
1391
- scheduler's \`max_concurrent_agents\`), NOT a wave cap.
1362
+ The tool ADMITTED this set against the live budget (and any declared in-flight cap):
1363
+ dispatch EXACTLY the ${rolling.session.frontier.length} node(s) below and no more.
1364
+ Their count is the whole grant — there is no separate concurrency cap. When they are
1365
+ all accepted, merge and re-invoke next-step; the tool re-grants the pending remainder.
1392
1366
 
1393
- ${renderDispatchDriverInstruction(driverSelection, `**${rolling.session.slots}**`)}
1367
+ ${renderDispatchDriverInstruction(driverSelection, `the ${rolling.session.frontier.length} granted node(s)`)}
1394
1368
 
1395
- Spawn ONE subagent for EACH initial node below. Give the subagent that node's
1369
+ Spawn ONE subagent for EACH granted node below. Give the subagent that node's
1396
1370
  \`prompt\`, and set its working directory to the node's **worktree** path. The
1397
1371
  subagent edits source files INSIDE that worktree and writes ONLY its result file.
1398
1372
  Do NOT let any subagent edit the main repository tree.
1399
1373
 
1400
- Initial nodes (worktrees already created):
1374
+ Granted nodes (worktrees already created):
1401
1375
  ${nodeLines}
1402
1376
 
1403
1377
  As EACH subagent finishes, run (substituting the finished node's block id):
@@ -1406,11 +1380,8 @@ As EACH subagent finishes, run (substituting the finished node's block id):
1406
1380
 
1407
1381
  It runs the commit -> verify -> merge lifecycle for that node and prints a JSON
1408
1382
  directive on stdout:
1409
- - \`{"directive":"dispatch","node":{...},"worktree_root":"..."}\` — spawn a subagent
1410
- for that next node (its worktree is already created), keeping up to
1411
- ${rolling.session.slots} running.
1412
- - \`{"directive":"wait",...}\` — other nodes are still in flight; do not spawn more yet.
1413
- - \`{"directive":"done",...}\` — every node has been accepted. Then run:
1383
+ - \`{"directive":"wait",...}\` — other granted nodes are still in flight; do not spawn more.
1384
+ - \`{"directive":"done",...}\` every granted node has been accepted. Then run:
1414
1385
 
1415
1386
  \`${rollMerge}\`
1416
1387
 
@@ -1431,8 +1402,8 @@ ${DISPATCH_PROMPT_HANDOFF_NOTE}
1431
1402
  }
1432
1403
  // Rolling per-node dispatch: prepare EVERY currently-eligible node (deps all
1433
1404
  // verified-complete), never a single artificially-serialized block. There is
1434
- // no wave-size cap — concurrency is owned by the quota scheduler
1435
- // (`dispatch-quota.json` max_concurrent_agents). `prepareImplementDispatch`
1405
+ // no wave-size cap — concurrency is emergent from admission control
1406
+ // (`dispatch-quota.json` `admission.granted_packet_ids`). `prepareImplementDispatch`
1436
1407
  // itself only admits verified-complete-eligible blocks
1437
1408
  // (`dependencyVerifiedComplete`), so this is the rolling-eligible frontier.
1438
1409
  const dispatchPlan = await prepareImplementDispatch({ root, artifactsDir }, runId, undefined, waveOptsImpl);
@@ -1504,15 +1475,17 @@ Read the dispatch plan and quota JSONs:
1504
1475
  \`${implQuotaPath}\`
1505
1476
 
1506
1477
  Every item in \`items\` is a node whose dependencies are all VERIFIED-COMPLETE
1507
- (INV-RS-01), so all of them are eligible to run now. Concurrency is owned by the
1508
- quota scheduler maintain up to \`max_concurrent_agents\` subagents running
1509
- simultaneously (from the quota file), with no separate wave-size cap. Each item's
1478
+ (INV-RS-01). The tool admitted a budget-bounded subset: dispatch EXACTLY the block
1479
+ ids in the quota file's \`admission.granted_packet_ids\` and no others — that granted
1480
+ set is the whole grant (there is no separate concurrency cap). Each item's
1510
1481
  \`model_hint.tier\` suggests which model to use (small/standard/deep). If your
1511
1482
  provider has rate limits, pace launches accordingly.
1512
1483
 
1513
- For each item in \`items\`, dispatch one subagent with that item's
1514
- \`prompt_path\`. Each subagent may edit source files needed for that bounded
1515
- block and must write only its assigned \`result_path\`.
1484
+ For each GRANTED item in \`items\` (its \`block_id\` in \`admission.granted_packet_ids\`),
1485
+ dispatch one subagent with that item's \`prompt_path\`. Each subagent may edit source
1486
+ files needed for that bounded block and must write only its assigned \`result_path\`.
1487
+ After the granted set is merged and you run next-step, the tool re-grants the pending
1488
+ remainder.
1516
1489
 
1517
1490
  ${SHARED_REBUILD_BETWEEN_LEVELS_NOTE}
1518
1491