@ai-dossier/sched 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -133,8 +133,9 @@ Two engine-safety policies were explicit product decisions on #464:
133
133
  an outage holds in `verifying` until truth returns. Each pause is journaled as
134
134
  `ground-truth-unreachable`.
135
135
 
136
- Only `issue:<n>` units are dispatched today — batch member sequencing is a follow-up
137
- (#464 non-goal).
136
+ This applies to `issue:<n>` unit dispatch (`dispatchAssignments`). `batch:<id>` units run
137
+ through a separate pass with its own claim/reconcile logic — see
138
+ [Batch dispatch (#523)](#batch-dispatch-523) below.
138
139
 
139
140
  ### Zombie-run fencing (#504)
140
141
 
@@ -182,9 +183,13 @@ but the unprotected redispatch is never silent.
182
183
  What happens when a batch's aggregate suite goes red, or its PR will not merge
183
184
  (RFC-0001 §F.2/F.8/F.9).
184
185
 
185
- **Not yet wired into `sched start`** — `tick()` still dispatches only `issue:<n>` units
186
- (the batch execution loop is a follow-up, see above). These modules are the library
187
- surface that loop will call, and they are tested standalone against real scratch repos.
186
+ **Wired into `sched start` since #523** the `validating attributing fixing/evicting`
187
+ rail below is called directly from `batch-dispatch.ts`'s `runValidate`/`evictOffender`
188
+ (a red AGGREGATE suite, after every member individually went green). A member that never
189
+ went green in the first place (its own gate failed) evicts through a separate, simpler
190
+ rail that never touches this module — see
191
+ [Batch dispatch (#523)](#batch-dispatch-523). These modules remain independently tested
192
+ against real scratch repos.
188
193
 
189
194
  ```
190
195
  validating → attributing → fixing (ONE bounded attempt) → validating
@@ -270,6 +275,64 @@ suspect-dispatch streak (#505 above). The two fields are a single fact and must
270
275
  on load: no suspect dispatches were ever tracked under them, so `0`/`null` is the exact
271
276
  backfill, not a guess.
272
277
 
278
+ ## Batch dispatch (#523)
279
+
280
+ `batch-dispatch.ts`'s `runBatchTick` — called from `tick()` after the issue-level pass,
281
+ only when `batchExec`/`runBatchSuite` are both configured on `EngineDeps` — drives every
282
+ `batch:<id>` unit through:
283
+
284
+ ```
285
+ ready → executing(member i/N) ⟲ → validating → reviewing → shipping
286
+ → awaiting-merge → merged → deployed → reported → done
287
+ failure rails: executing → dissolving (a member self-reports blocked)
288
+ validating → attributing → (fixing | evicting) → validating → dissolving
289
+ ```
290
+
291
+ - **One shared worktree/branch per batch**, claimed once by a deterministic (no LLM)
292
+ `batch-setup` step: `git branch`/`push`/`worktree add` off `base_branch`, named
293
+ `batch/<id>-<date>`, plus a fresh `ai-dossier runstate mint` against the anchor issue.
294
+ - **Members run serially, one fresh `slot-cycle` agent at a time**, in the shared
295
+ worktree. A member's completion signal is `phase=review status=done mode=slot` on its
296
+ OWN issue (`slot-cycle` posts no phase of its own past `review` — ship is batch-owned);
297
+ its commit range on the batch branch is recomputed (`git log`) after every member and
298
+ kept on `BatchEntry.ranges` for eviction. An incremental gate (`ai-dossier cap run
299
+ typecheck.run` / `test.focused`, when the repo has a manifest) runs after each member
300
+ before advancing — a second, independent check that the member's self-reported "done"
301
+ is real.
302
+ - **The batch's single slot is claimed FRESH for each live step** (a member, the tail
303
+ agent, the report agent, a bounded fix agent) — never held across a wait. The aggregate
304
+ suite itself runs with NO slot claimed at all (deterministic engine work, not an LLM
305
+ step).
306
+ - **Two failure rails.** A member that never went green evicts directly (nothing to
307
+ attribute — see the #472 section above for what "directly" skips). A red AGGREGATE
308
+ suite (every member individually green, but integration-level conflict) routes through
309
+ the #472 attribution/fix/evict library.
310
+ - **The tail**, after the last member: the aggregate suite runs deterministically; green
311
+ spawns ONE bounded strong-tier agent that runs `review-issue` aggregate mode then
312
+ `ship-issue` batch mode (rebase-merge, a `Closes` list) and parks the PR exactly like a
313
+ detached full-cycle run; the engine's own PR watcher (a batch-granularity mirror of the
314
+ per-issue one) accepts the merge and dispatches a cheap mechanical-tier agent for
315
+ `report-issue`'s batch variant.
316
+ - **Scope cuts, recorded rather than discovered later:** no `git bisect` stage for an
317
+ ambiguous aggregate failure (an unattributable red suite dissolves instead); no
318
+ worktree-pool integration for batch-setup (cold `git worktree add` only); no per-phase
319
+ stall/escalation ladder for batch sub-agents (a dead-without-verification agent is
320
+ treated as blocked, not redispatched stronger).
321
+
322
+ Schema 1.7.0: `BatchEntry` gains `worktree` (absolute path of the shared batch worktree,
323
+ null until batch-setup lands), `ranges` (`MemberRange[]` — each member's commit range,
324
+ recomputed after every member completes) and `pr` (the batch PR parked on auto-merge,
325
+ persisted so a restart mid-watch still knows what to poll). 1.6.0 states migrate on load:
326
+ no batch was ever dispatched under them, so `null`/`[]`/`null` is the exact backfill, not
327
+ a guess. Config schema moves to 1.3.0: `dispatch` gains `member_prompt`,
328
+ `batch_tail_prompt` and `batch_report_prompt` (the three new agent prompt templates).
329
+
330
+ New journal events: `batch-setup-done`, `batch-setup-failed`, `member-advanced`. Member/
331
+ tail/report/fix-agent spawn, progress, completion and park events reuse the existing
332
+ unit-generic names (`assigned`/`spawned`/`unit-failed`/`external-advance`/`pr-parked`/
333
+ `merge-accepted`/`report-dispatched`/`teardown-done`/`teardown-failed`) with
334
+ `unit = batch:<id>`.
335
+
273
336
  ## API surface
274
337
 
275
338
  ```ts
@@ -284,9 +347,11 @@ import {
284
347
  runLoop, // the sched start loop (tick, sleep, repeat)
285
348
  type TickResult, // what one tick did (spawned/parked/merge-accepted/stale-reconciled/
286
349
  // dependents-unblocked/report-dispatched/teardown/completed/
287
- // redispatched/failed/blocked)
350
+ // redispatched/failed/blocked) — since #523 also carries
351
+ // `batch:<id>` unit ids (issue numbers for `blocked`)
288
352
  type EngineDeps, // inject everything the engine touches (store/journal/spawn/ground
289
- // truth/clock/repoDir/teardownExec/fencer)
353
+ // truth/clock/repoDir/teardownExec/fencer/batchExec/runBatchSuite/
354
+ // runBatchCapability — #523)
290
355
  createSpawnDeps, // real detached-spawn process I/O
291
356
  createExecGroundTruth, // runstate/gh/git ground truth via subprocesses (injectable exec);
292
357
  // since #468 also gh pr view PR state + setup info from comments
@@ -337,9 +402,24 @@ import {
337
402
  transitionIssue, transitionBatch, transitionSlot, // typed §D transitions
338
403
  TRANSITIONS, // the transition tables themselves (for previews)
339
404
  buildStatusReport, // machine-readable status incl. blocked/failed sets
340
- validateState, // strict persisted-state validation (1.0.0-1.5.0 files migrate)
405
+ validateState, // strict persisted-state validation (1.0.0-1.6.0 files migrate)
341
406
  IllegalTransitionError, EnqueueError, CorruptStateError, LockTimeoutError,
342
407
  SchedNotFoundError,
408
+ runBatchTick, // #523: one batch reconcile+refill pass; called by tick() after
409
+ // the issue pass — loads/saves state itself, holds no lock
410
+ // across the call
411
+ type BatchDispatchDeps, // inject store/journal/groundTruth/spawnDeps/exec/runSuite/
412
+ // runCapability(optional)/fsExists(optional)
413
+ type BatchTickResult, // spawned/completed/parked/mergeAccepted/failed (batch:<id> ids)
414
+ // + blocked (issue numbers, dissolve-requeued)
415
+ type CapOutcome, // ok | task-failed | automation-broken | capability-unavailable
416
+ buildMemberPrompt, buildBatchTailPrompt, buildBatchReportPrompt, // #523 prompt builders
417
+ DEFAULT_MEMBER_PROMPT_TEMPLATE, DEFAULT_BATCH_TAIL_PROMPT_TEMPLATE,
418
+ DEFAULT_BATCH_REPORT_PROMPT_TEMPLATE,
419
+ isMemberComplete, isMemberBlocked, // member milestone predicates (mode=slot gated)
420
+ isBatchTailParked, // batch-ship awaiting-merge + pr= — the batch park signal
421
+ isBatchPhaseDone, // <phase> done on the anchor (batch-review/batch-report)
422
+ batchOfUnit, // batch:<id> → <id>; null for issue units or malformed ids
343
423
  } from '@ai-dossier/sched';
344
424
  ```
345
425
 
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Batch dispatch (#523, RFC-0001 §C.4/D.2/D.3): the missing driver that
3
+ * executes `batch:<id>` units. #498 landed the batch failure-recovery library
4
+ * (attribution/bisect/eviction/dissolve, `recovery.ts`) and the batch state
5
+ * machine (`state.ts`); readiness/placement already treat a `ready` batch as
6
+ * a runnable unit (`readiness.ts`, `scheduler.ts`). Nothing dispatched one
7
+ * until now.
8
+ *
9
+ * Shape, mirroring `engine.ts`'s per-issue dispatch: claim a slot → spawn an
10
+ * agent → poll ground truth → verify → transition. Generalized to `BatchEntry`
11
+ * at batch-phase granularity instead of per-issue-phase granularity:
12
+ *
13
+ * ```
14
+ * ready → executing(member i/N) ⟲ → validating → reviewing → shipping
15
+ * → awaiting-merge → merged → deployed → reported → done
16
+ * failure rails (RFC F.2/F.8/F.9):
17
+ * executing → dissolving (a member self-reports blocked, RFC F.1)
18
+ * validating → attributing → (fixing | evicting) → validating
19
+ * → dissolving
20
+ * ```
21
+ *
22
+ * NO batch claim — not the first (`ready → executing`) nor any continuation
23
+ * (a later member, the tail agent, the report agent, the fix agent) — ever
24
+ * goes through `computeAssignments`/`runnableUnits`. Every one is a bespoke
25
+ * free-capacity-gated assignment, the same shape `engine.ts`'s
26
+ * `dispatchReportAgents` already uses (`runnableUnits` only ever offers a
27
+ * `status === 'ready'` batch, i.e. the moment BEFORE any claim). Between
28
+ * steps — a suite run, a PR merge wait — the slot is released to `idle` and
29
+ * holds no capacity (AC5): only a live member/tail/report/fix agent holds a
30
+ * slot.
31
+ *
32
+ * The aggregate suite itself is deterministic engine work, not an LLM step —
33
+ * it runs with no slot claimed at all, matching AC5's "member or batch-LLM-step"
34
+ * wording precisely.
35
+ *
36
+ * Two distinct failure rails, deliberately different:
37
+ * - A member's OWN agent reports itself blocked (its own gate never went
38
+ * green) — evicted directly, no attribution needed: the offender is already
39
+ * known, and either it has no commits yet (blocked before implementing) or
40
+ * its commits are exactly what gets reverted.
41
+ * - The AGGREGATE suite (run by the engine after every member individually
42
+ * went green) comes back red — an integration-level conflict no member's own
43
+ * gate caught. THIS is what `recovery.ts`'s attribution/fix/evict pipeline
44
+ * exists for (RFC F.2).
45
+ *
46
+ * Scope decisions recorded here, not silently cut: no `git bisect` stage for
47
+ * an ambiguous aggregate failure (bisect needs a per-project "run only these
48
+ * tests" command this module has no generic way to construct) — an
49
+ * unattributable red aggregate suite dissolves the batch rather than
50
+ * bisecting, which `attributing → dissolving` already models. No worktree-pool
51
+ * integration for batch-setup — cold `git worktree add` only, mirroring
52
+ * `teardown.ts`'s cold path. No per-phase stall/escalation ladder for batch
53
+ * sub-agents — a dead-without-verification agent is treated as blocked and
54
+ * evicted/reported rather than redispatched stronger. Both are documented
55
+ * follow-ups, not gaps discovered later.
56
+ */
57
+ import { type ResolvedDispatch, type SpawnDeps } from './dispatch';
58
+ import { type GroundTruth } from './groundtruth';
59
+ import { type Journal } from './journal';
60
+ import type { SchedStore } from './persist';
61
+ import type { ExecFn } from './project';
62
+ import { type SuiteResult } from './recovery';
63
+ import { type FsExists } from './teardown';
64
+ import type { SchedConfig } from './types';
65
+ /**
66
+ * The four `ai-dossier cap run` outcomes (docs/reference/capabilities.md):
67
+ * `ok` = the capability ran and passed; `task-failed` = it ran and the TASK
68
+ * itself failed (trust the result); `automation-broken` = do not trust the
69
+ * machinery (missing tool, timeout, bad manifest); `capability-unavailable` =
70
+ * no manifest / no such id / `lifecycle: shadow` — no fast path here.
71
+ */
72
+ export type CapOutcome = 'ok' | 'task-failed' | 'automation-broken' | 'capability-unavailable';
73
+ /** Everything batch dispatch needs from the outside world. */
74
+ export interface BatchDispatchDeps {
75
+ store: SchedStore;
76
+ journal: Journal;
77
+ groundTruth: GroundTruth;
78
+ spawnDeps: SpawnDeps;
79
+ now: () => Date;
80
+ /** Repo root — cwd for `git`/`ai-dossier` calls that are not batch-worktree-scoped. */
81
+ repoDir: string;
82
+ /** Exec for batch git/milestone-CLI operations (never throws — the `ExecFn` contract). */
83
+ exec: ExecFn;
84
+ /** Runs the aggregate suite inside a batch worktree; batches never leave `validating` without one. */
85
+ runSuite: (worktree: string) => SuiteResult;
86
+ /**
87
+ * Runs one `ai-dossier cap run <capabilityId>` in a batch worktree for the
88
+ * per-member incremental gate (#523 AC2 — "typecheck + focused tests via
89
+ * `cap run test.focused` when available"). Optional and degrade-not-crash,
90
+ * like `batchExec`/`runBatchSuite`: without it, or on `automation-broken`/
91
+ * `capability-unavailable`, the gate is skipped — the member's own
92
+ * `slot-cycle` run already attempted this fast path (with its own reasoning
93
+ * fallback) before ever posting `review done`, so a repo with no manifest
94
+ * loses nothing but the engine's independent re-check.
95
+ */
96
+ runCapability?: (worktree: string, capabilityId: string) => CapOutcome;
97
+ fsExists?: FsExists;
98
+ }
99
+ /** What one `runBatchTick` call did, merged into `engine.ts`'s `TickResult` by the caller. */
100
+ export interface BatchTickResult {
101
+ spawned: string[];
102
+ completed: string[];
103
+ parked: string[];
104
+ mergeAccepted: string[];
105
+ failed: string[];
106
+ /** Issue numbers requeued full-cycle by a dissolve — matches `TickResult.blocked`'s shape. */
107
+ blocked: number[];
108
+ }
109
+ /**
110
+ * One batch reconcile+refill pass. Called from `engine.ts`'s `tick()` after
111
+ * the issue-level pass — batches never compete with issues for a slot within
112
+ * the same tick because this pass runs strictly after `dispatchAssignments`
113
+ * already filled every slot it could (see the module doc: batch claims never
114
+ * go through `computeAssignments`/`runnableUnits` at all). Loads and saves
115
+ * state itself via `deps.store.withLock` — the caller holds no lock across
116
+ * this call. `deps.exec` and `deps.runSuite` are mandatory; `deps.
117
+ * runCapability` is independently optional (AC2's incremental gate is itself
118
+ * a "when available" fast path).
119
+ */
120
+ export declare function runBatchTick(deps: BatchDispatchDeps, config: SchedConfig, dispatch: ResolvedDispatch): BatchTickResult;
121
+ //# sourceMappingURL=batch-dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batch-dispatch.d.ts","sourceRoot":"","sources":["../src/batch-dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAWH,OAAO,EAKL,KAAK,gBAAgB,EACrB,KAAK,SAAS,EAEf,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,KAAK,WAAW,EAOjB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,OAAO,EAAa,MAAM,WAAW,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EASL,KAAK,WAAW,EACjB,MAAM,YAAY,CAAC;AAWpB,OAAO,EAAE,KAAK,QAAQ,EAA+B,MAAM,YAAY,CAAC;AACxE,OAAO,KAAK,EAKV,WAAW,EAIZ,MAAM,SAAS,CAAC;AAEjB;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,aAAa,GAAG,mBAAmB,GAAG,wBAAwB,CAAC;AAE/F,8DAA8D;AAC9D,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,UAAU,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,WAAW,CAAC;IACzB,SAAS,EAAE,SAAS,CAAC;IACrB,GAAG,EAAE,MAAM,IAAI,CAAC;IAChB,uFAAuF;IACvF,OAAO,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,IAAI,EAAE,MAAM,CAAC;IACb,sGAAsG;IACtG,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,WAAW,CAAC;IAC5C;;;;;;;;;OASG;IACH,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,UAAU,CAAC;IACvE,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED,8FAA8F;AAC9F,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,8FAA8F;IAC9F,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAyuCD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,iBAAiB,EACvB,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,gBAAgB,GACzB,eAAe,CAsEjB"}