audit-tools 0.32.8 → 0.32.10

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 (114) hide show
  1. package/README.md +4 -2
  2. package/dist/audit/cli/dispatch/quotaPool.d.ts +20 -2
  3. package/dist/audit/cli/dispatch/quotaPool.d.ts.map +1 -1
  4. package/dist/audit/cli/dispatch/quotaPool.js +38 -7
  5. package/dist/audit/cli/dispatch/quotaPool.js.map +1 -1
  6. package/dist/audit/cli/dispatch/tierRouting.d.ts +4 -1
  7. package/dist/audit/cli/dispatch/tierRouting.d.ts.map +1 -1
  8. package/dist/audit/cli/dispatch/tierRouting.js +7 -4
  9. package/dist/audit/cli/dispatch/tierRouting.js.map +1 -1
  10. package/dist/audit/cli/dispatch/types.d.ts +9 -4
  11. package/dist/audit/cli/dispatch/types.d.ts.map +1 -1
  12. package/dist/audit/cli/dispatch.d.ts +7 -0
  13. package/dist/audit/cli/dispatch.d.ts.map +1 -1
  14. package/dist/audit/cli/dispatch.js +24 -12
  15. package/dist/audit/cli/dispatch.js.map +1 -1
  16. package/dist/audit/cli/mergeAndIngestCommand.d.ts +0 -9
  17. package/dist/audit/cli/mergeAndIngestCommand.d.ts.map +1 -1
  18. package/dist/audit/cli/mergeAndIngestCommand.js +29 -1
  19. package/dist/audit/cli/mergeAndIngestCommand.js.map +1 -1
  20. package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
  21. package/dist/audit/cli/nextStepCommand.js +23 -8
  22. package/dist/audit/cli/nextStepCommand.js.map +1 -1
  23. package/dist/audit/cli/prompts.d.ts +36 -2
  24. package/dist/audit/cli/prompts.d.ts.map +1 -1
  25. package/dist/audit/cli/prompts.js +44 -5
  26. package/dist/audit/cli/prompts.js.map +1 -1
  27. package/dist/audit/cli/rollingAuditDispatch.d.ts +18 -1
  28. package/dist/audit/cli/rollingAuditDispatch.d.ts.map +1 -1
  29. package/dist/audit/cli/rollingAuditDispatch.js +25 -1
  30. package/dist/audit/cli/rollingAuditDispatch.js.map +1 -1
  31. package/dist/audit/cli/semanticReviewStep.d.ts.map +1 -1
  32. package/dist/audit/cli/semanticReviewStep.js +34 -9
  33. package/dist/audit/cli/semanticReviewStep.js.map +1 -1
  34. package/dist/audit/cli/steps.d.ts +22 -12
  35. package/dist/audit/cli/steps.d.ts.map +1 -1
  36. package/dist/audit/cli/steps.js +4 -2
  37. package/dist/audit/cli/steps.js.map +1 -1
  38. package/dist/audit/orchestrator/rollingDispatch.d.ts +7 -6
  39. package/dist/audit/orchestrator/rollingDispatch.d.ts.map +1 -1
  40. package/dist/audit/orchestrator/rollingDispatch.js +37 -19
  41. package/dist/audit/orchestrator/rollingDispatch.js.map +1 -1
  42. package/dist/audit/quota/index.d.ts +137 -8
  43. package/dist/audit/quota/index.d.ts.map +1 -1
  44. package/dist/audit/quota/index.js +9 -8
  45. package/dist/audit/quota/index.js.map +1 -1
  46. package/dist/remediate/dispatch/ownershipRegistry.d.ts +2 -22
  47. package/dist/remediate/dispatch/ownershipRegistry.d.ts.map +1 -1
  48. package/dist/remediate/dispatch/ownershipRegistry.js +7 -45
  49. package/dist/remediate/dispatch/ownershipRegistry.js.map +1 -1
  50. package/dist/remediate/steps/dispatch.d.ts +22 -1
  51. package/dist/remediate/steps/dispatch.d.ts.map +1 -1
  52. package/dist/remediate/steps/dispatch.js +89 -5
  53. package/dist/remediate/steps/dispatch.js.map +1 -1
  54. package/dist/remediate/steps/nextStep.d.ts.map +1 -1
  55. package/dist/remediate/steps/nextStep.js +89 -129
  56. package/dist/remediate/steps/nextStep.js.map +1 -1
  57. package/dist/remediate/steps/rollingSession.d.ts +15 -17
  58. package/dist/remediate/steps/rollingSession.d.ts.map +1 -1
  59. package/dist/remediate/steps/rollingSession.js +33 -57
  60. package/dist/remediate/steps/rollingSession.js.map +1 -1
  61. package/dist/remediate/steps/types.d.ts +12 -4
  62. package/dist/remediate/steps/types.d.ts.map +1 -1
  63. package/dist/remediate/steps/types.js +1 -1
  64. package/dist/shared/dispatch/admissionLoop.d.ts +245 -0
  65. package/dist/shared/dispatch/admissionLoop.d.ts.map +1 -0
  66. package/dist/shared/dispatch/admissionLoop.js +204 -0
  67. package/dist/shared/dispatch/admissionLoop.js.map +1 -0
  68. package/dist/{remediate → shared}/dispatch/ownershipScheduler.d.ts +23 -2
  69. package/dist/shared/dispatch/ownershipScheduler.d.ts.map +1 -0
  70. package/dist/{remediate → shared}/dispatch/ownershipScheduler.js +34 -6
  71. package/dist/shared/dispatch/ownershipScheduler.js.map +1 -0
  72. package/dist/shared/dispatch/pathIdentity.d.ts +26 -0
  73. package/dist/shared/dispatch/pathIdentity.d.ts.map +1 -0
  74. package/dist/shared/dispatch/pathIdentity.js +49 -0
  75. package/dist/shared/dispatch/pathIdentity.js.map +1 -0
  76. package/dist/shared/dispatch/rollingDispatch.d.ts +37 -0
  77. package/dist/shared/dispatch/rollingDispatch.d.ts.map +1 -1
  78. package/dist/shared/dispatch/rollingDispatch.js +117 -18
  79. package/dist/shared/dispatch/rollingDispatch.js.map +1 -1
  80. package/dist/shared/dispatch/unifiedRolling.d.ts +131 -0
  81. package/dist/shared/dispatch/unifiedRolling.d.ts.map +1 -0
  82. package/dist/shared/dispatch/unifiedRolling.js +161 -0
  83. package/dist/shared/dispatch/unifiedRolling.js.map +1 -0
  84. package/dist/shared/index.d.ts +13 -2
  85. package/dist/shared/index.d.ts.map +1 -1
  86. package/dist/shared/index.js +19 -2
  87. package/dist/shared/index.js.map +1 -1
  88. package/dist/shared/quota/claimLease.d.ts.map +1 -1
  89. package/dist/shared/quota/claimLease.js +8 -1
  90. package/dist/shared/quota/claimLease.js.map +1 -1
  91. package/dist/shared/quota/packetCost.d.ts +29 -0
  92. package/dist/shared/quota/packetCost.d.ts.map +1 -0
  93. package/dist/shared/quota/packetCost.js +51 -0
  94. package/dist/shared/quota/packetCost.js.map +1 -0
  95. package/dist/shared/quota/reservationLedger.d.ts +100 -0
  96. package/dist/shared/quota/reservationLedger.d.ts.map +1 -0
  97. package/dist/shared/quota/reservationLedger.js +224 -0
  98. package/dist/shared/quota/reservationLedger.js.map +1 -0
  99. package/dist/shared/quota/state.d.ts +20 -0
  100. package/dist/shared/quota/state.d.ts.map +1 -1
  101. package/dist/shared/quota/state.js +50 -0
  102. package/dist/shared/quota/state.js.map +1 -1
  103. package/dist/shared/quota/types.d.ts +11 -0
  104. package/dist/shared/quota/types.d.ts.map +1 -1
  105. package/dist/shared/quota/types.js.map +1 -1
  106. package/dist/shared/types/sessionConfig.d.ts +10 -0
  107. package/dist/shared/types/sessionConfig.d.ts.map +1 -1
  108. package/dist/shared/types/sessionConfig.js +13 -0
  109. package/dist/shared/types/sessionConfig.js.map +1 -1
  110. package/docs/audit-pkg/contracts.md +5 -4
  111. package/docs/audit-pkg/operator-guide.md +9 -10
  112. package/package.json +1 -1
  113. package/dist/remediate/dispatch/ownershipScheduler.d.ts.map +0 -1
  114. 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, resolveHostProviderName } 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";
@@ -39,19 +38,6 @@ import { ambiguityReviewPrompt, clarificationPrompt, collectIntakeClarifications
39
38
  // from the canonical enum (the very drift `types/lens.ts` exists to prevent).
40
39
  const VALID_LENSES_PROSE = LENSES.map((lens) => `\`${lens}\``).join(", ");
41
40
  const VALID_SEVERITIES_PROSE = SEVERITIES.map((sev) => `\`${sev}\``).join(", ");
42
- /**
43
- * The host's resolved provider identity for quota-key / driver-classification
44
- * purposes: the configured `sessionConfig.provider`, defaulting to the
45
- * conversation host (`claude-code`) when unset or `auto`. Single-sourced so the
46
- * `?? "claude-code"` default and the `auto`-exclusion live in ONE place rather
47
- * than being re-spelled (with an ad-hoc cast) at each dispatch call site.
48
- */
49
- function resolveHostProviderName(sessionConfig) {
50
- const provider = sessionConfig?.provider;
51
- if (provider === undefined || provider === "auto")
52
- return "claude-code";
53
- return provider;
54
- }
55
41
  export function resolveHostDispatchCapability(options) {
56
42
  if (options.hostCanDispatchSubagents !== undefined) {
57
43
  return options.hostCanDispatchSubagents;
@@ -339,9 +325,9 @@ function blockedByUnsatisfiedDependency(state) {
339
325
  // The rolling scheduler replaced the wave-batch shim: instead of dispatching one
340
326
  // fixed-size wave per next-step and folding back through `implementing`, a node
341
327
  // 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.
328
+ // disposition (INV-RS-01). Concurrency is emergent from admission control (the
329
+ // `dispatch-quota.json` `admission.granted_packet_ids` the tool admits against the
330
+ // live budget — INV-S05 / INV-QD-11), never a computed wave cap.
345
331
  // A shared rebuild is interposed between dependency levels so a downstream node
346
332
  // typechecks/runs against the freshly-built upstream `audit-tools/shared`
347
333
  // surface; the rebuild is single-flight (CE-001) so the same package is never
@@ -495,103 +481,69 @@ export async function driveRollingDispatch(levels, options) {
495
481
  setQuotaStateDir(options.quotaStateDir);
496
482
  }
497
483
  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;
484
+ const scopeForBlock = options.scopeForBlock ?? ((b) => b.touched_files ?? []);
485
+ const allBlocks = levels.flat();
486
+ const blockById = new Map(allBlocks.map((b) => [b.block_id, b]));
487
+ const hostSession = options.hostSession;
488
+ // Admission control: when a metered pool reports a finite absolute budget, the shared
489
+ // reservation ledger leases each node's cost before dispatch, so co-located dispatch
490
+ // loops on one account (a second IDE, the host + its subagents) never collectively
491
+ // over-admit. On the claude-code host (percent-only quota, no finite ceiling) the ledger
492
+ // is omitted and the reactive 429 floor is the safety no per-dispatch lock overhead.
493
+ const ledgerCfg = resolveLedgerBudgets({
494
+ pools: options.confirmedPools,
495
+ sessionConfig: options.sessionConfig,
496
+ pendingItemTokens: allBlocks.map((b) => estimateTokens(b)),
497
+ });
498
+ // The remediate terminal adapter over the unified shared driver. What stays here is
499
+ // remediate's projection: a block → its ownership node (declared write-scope, so the
500
+ // shared `ownershipSubWaves` serializes same-file nodes / parallelizes disjoint ones)
501
+ // and → its dispatch packet; the shared rebuild between dependency levels; and the
502
+ // host-session escalation wiring. The loop, sub-wave split, and quota_paused/empty_pool
503
+ // terminal merge are the unified driver's.
504
+ const run = await driveRolling({
505
+ levels,
506
+ confirmedPools: options.confirmedPools,
507
+ sessionConfig: options.sessionConfig,
508
+ ...(ledgerCfg.reservationLedger
509
+ ? {
510
+ reservationLedger: ledgerCfg.reservationLedger,
511
+ resolvePoolBudget: ledgerCfg.resolvePoolBudget,
512
+ resolveOutputReservation: (_packet, poolId) => ledgerCfg.resolveOutputReservation(poolId),
515
513
  }
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) => ({
514
+ : {}),
515
+ toNode: (b) => ({
526
516
  block_id: b.block_id,
527
517
  write_paths: scopeForBlock(b),
528
518
  ...(b.cofile_parallel_safe !== undefined
529
519
  ? { cofile_parallel_safe: b.cofile_parallel_safe }
530
520
  : {}),
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 };
521
+ }),
522
+ toPacket: (b) => ({
523
+ id: b.block_id,
524
+ payload: { block_id: b.block_id },
525
+ estimatedTokens: estimateTokens(b),
526
+ complexity: 0.5,
527
+ }),
528
+ dispatchPacket: async (packet, slot) => options.dispatchNode(blockById.get(packet.payload.block_id), slot),
529
+ ...(options.root !== undefined ? { root: options.root } : {}),
530
+ // Single-flight (CE-001) is enforced by the unified driver.
531
+ rebuildBetweenLevels: options.rebuildSharedBetweenLevels,
532
+ // Host-session escalation: feed recordLimit (write) at the rate_limited observation
533
+ // point and read isEscalated (strand-not-requeue). The SAME instance sized the pools,
534
+ // so the bounded re-limit count is account-wide.
535
+ ...(hostSession
536
+ ? {
537
+ recordRateLimit: (packet, result) => hostSession.recordLimit(result.rateLimit?.channel ?? "error", result.rateLimit?.text ?? "", packet.id),
538
+ isPacketEscalated: (packetId) => hostSession.isEscalated(packetId),
539
+ }
540
+ : {}),
541
+ });
542
+ return {
543
+ levels: run.levels.map((l) => ({ blockIds: l.nodeIds, results: l.results })),
544
+ rebuilds: run.rebuilds,
545
+ ...(run.terminal ? { terminal: run.terminal } : {}),
546
+ };
595
547
  }
596
548
  /**
597
549
  * Backends the orchestrator can drive IN-PROCESS as the per-node implement worker
@@ -665,6 +617,10 @@ export async function driveRollingImplementDispatch(options) {
665
617
  hostModelId: options.waveOptions?.hostModelId,
666
618
  // Each node runs in its own worktree, so its prompt is rooted there.
667
619
  worktreeRootedPrompts: true,
620
+ // The in-process rolling engine admits + leases per-packet itself, so the
621
+ // dispatch-quota grant here must NOT lease (a host grant lease would
622
+ // double-count the same work against the shared account budget).
623
+ grantLeases: false,
668
624
  });
669
625
  if (plan.items.length === 0) {
670
626
  return null;
@@ -1366,7 +1322,10 @@ async function buildImplementDispatchStep(ctx) {
1366
1322
  const driverSelection = selectDispatchDriver({
1367
1323
  classification: classifyProvider(hostProvider),
1368
1324
  eligibleItemCount: rolling.session.frontier.length,
1369
- slots: rolling.session.slots,
1325
+ // The granted set's size IS the instantaneous admission width — there is no
1326
+ // separate concurrency number. The whole granted set runs at once, so the
1327
+ // driver-selection "slots" is the granted-set size.
1328
+ slots: rolling.session.frontier.length,
1370
1329
  });
1371
1330
  const rollMerge = loaderCommand(`merge-implement-results --run-id ${runId}`);
1372
1331
  const rollNext = loaderCommand("next-step");
@@ -1383,21 +1342,23 @@ async function buildImplementDispatchStep(ctx) {
1383
1342
  prompt: `
1384
1343
  # Dispatch Implementation Work (host-subagent rolling, worktree-isolated)
1385
1344
 
1386
- Each eligible node runs in its OWN git worktree (hard isolation between nodes). The
1345
+ Each granted node runs in its OWN git worktree (hard isolation between nodes). The
1387
1346
  TOOL owns commit -> verify -> merge + write-scope; you only spawn a subagent per
1388
1347
  node and call \`accept-node\` as each finishes.
1389
1348
 
1390
- Concurrency target: **${rolling.session.slots}** subagents at once (the quota
1391
- scheduler's \`max_concurrent_agents\`), NOT a wave cap.
1349
+ The tool ADMITTED this set against the live budget (and any declared in-flight cap):
1350
+ dispatch EXACTLY the ${rolling.session.frontier.length} node(s) below and no more.
1351
+ Their count is the whole grant — there is no separate concurrency cap. When they are
1352
+ all accepted, merge and re-invoke next-step; the tool re-grants the pending remainder.
1392
1353
 
1393
- ${renderDispatchDriverInstruction(driverSelection, `**${rolling.session.slots}**`)}
1354
+ ${renderDispatchDriverInstruction(driverSelection, `the ${rolling.session.frontier.length} granted node(s)`)}
1394
1355
 
1395
- Spawn ONE subagent for EACH initial node below. Give the subagent that node's
1356
+ Spawn ONE subagent for EACH granted node below. Give the subagent that node's
1396
1357
  \`prompt\`, and set its working directory to the node's **worktree** path. The
1397
1358
  subagent edits source files INSIDE that worktree and writes ONLY its result file.
1398
1359
  Do NOT let any subagent edit the main repository tree.
1399
1360
 
1400
- Initial nodes (worktrees already created):
1361
+ Granted nodes (worktrees already created):
1401
1362
  ${nodeLines}
1402
1363
 
1403
1364
  As EACH subagent finishes, run (substituting the finished node's block id):
@@ -1406,11 +1367,8 @@ As EACH subagent finishes, run (substituting the finished node's block id):
1406
1367
 
1407
1368
  It runs the commit -> verify -> merge lifecycle for that node and prints a JSON
1408
1369
  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:
1370
+ - \`{"directive":"wait",...}\` — other granted nodes are still in flight; do not spawn more.
1371
+ - \`{"directive":"done",...}\` every granted node has been accepted. Then run:
1414
1372
 
1415
1373
  \`${rollMerge}\`
1416
1374
 
@@ -1431,8 +1389,8 @@ ${DISPATCH_PROMPT_HANDOFF_NOTE}
1431
1389
  }
1432
1390
  // Rolling per-node dispatch: prepare EVERY currently-eligible node (deps all
1433
1391
  // 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`
1392
+ // no wave-size cap — concurrency is emergent from admission control
1393
+ // (`dispatch-quota.json` `admission.granted_packet_ids`). `prepareImplementDispatch`
1436
1394
  // itself only admits verified-complete-eligible blocks
1437
1395
  // (`dependencyVerifiedComplete`), so this is the rolling-eligible frontier.
1438
1396
  const dispatchPlan = await prepareImplementDispatch({ root, artifactsDir }, runId, undefined, waveOptsImpl);
@@ -1504,15 +1462,17 @@ Read the dispatch plan and quota JSONs:
1504
1462
  \`${implQuotaPath}\`
1505
1463
 
1506
1464
  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
1465
+ (INV-RS-01). The tool admitted a budget-bounded subset: dispatch EXACTLY the block
1466
+ ids in the quota file's \`admission.granted_packet_ids\` and no others — that granted
1467
+ set is the whole grant (there is no separate concurrency cap). Each item's
1510
1468
  \`model_hint.tier\` suggests which model to use (small/standard/deep). If your
1511
1469
  provider has rate limits, pace launches accordingly.
1512
1470
 
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\`.
1471
+ For each GRANTED item in \`items\` (its \`block_id\` in \`admission.granted_packet_ids\`),
1472
+ dispatch one subagent with that item's \`prompt_path\`. Each subagent may edit source
1473
+ files needed for that bounded block and must write only its assigned \`result_path\`.
1474
+ After the granted set is merged and you run next-step, the tool re-grants the pending
1475
+ remainder.
1516
1476
 
1517
1477
  ${SHARED_REBUILD_BETWEEN_LEVELS_NOTE}
1518
1478