@ai-dossier/sched 0.51.0 → 0.52.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
@@ -31,6 +31,7 @@ ai-dossier sched start --once # a single reconcile+refill tick (cron-style)
31
31
  ai-dossier sched status # queue (+pr/cleanup), engine lease, slots, batches, blocked/failed; --anchors adds the ledger anchor sweep (#768) plus orphaned anchors no longer in state.batches (#790)
32
32
  ai-dossier sched pause # prevent every new agent process; live units keep running
33
33
  ai-dossier sched resume
34
+ ai-dossier sched resume --batch b1 # recheck a gate-inconclusive block (#583), or re-run gate + tail over a batch blocked over landed work (#822)
34
35
  ai-dossier sched stop --issue 42 # terminate one full-cycle agent (or stop a parked batch member, #832) and record it stopped (no recovery)
35
36
  ai-dossier sched stop --batch b1 # terminate every batch agent (incl. parallel members) and stop unfinished members
36
37
  ai-dossier sched abandon --issue 42 --reason "operator abort"
@@ -771,6 +772,9 @@ failure rails: executing → dissolving (a member self-reports blocked)
771
772
  validating → blocked (suite report unreadable, #562) → validating
772
773
  executing → blocked (gate-inconclusive:<cap>, #583) → executing
773
774
  (`sched resume --batch <id>` re-runs the gate; nothing requeued/reverted)
775
+ (dissolve refused | tail) → blocked (dissolve-refused:* / tail-blocked:* /
776
+ members-mismatch:* / respawn-cap:tail) → validating
777
+ (`sched resume --batch <id>` re-runs gate + tail over the landed members, #822)
774
778
  ```
775
779
 
776
780
  - **One shared worktree/branch per batch**, claimed once by a deterministic (no LLM)
@@ -1004,13 +1008,15 @@ Dissolve never throws validated work away:
1004
1008
  `gh pr create --head <integration branch> --base <base>` — once it merges,
1005
1009
  `reconcileStaleBlockedBatches` settles the validated members to shipped; for a red suite
1006
1010
  or a partial revert, inspect/fix the branch first; `sched abandon --batch` requeues them
1007
- full-cycle instead. (A `sched resume --batch` arm for these blocks is a follow-up.) Without validated members it dissolves
1011
+ full-cycle instead. Since #822, `sched resume --batch <id>` re-runs the gate and the tail
1012
+ over the validated members instead (see [below](#resuming-a-batch-blocked-over-landed-work-822)). Without validated members it dissolves
1008
1013
  as before, except that in-flight members with a member branch are parked `evicted` and
1009
1014
  every requeue carries the batch's dispatch profile (`carryDispatchProfile: false` only
1010
1015
  for `dispatch-profile-missing:*`, where the profile itself is what broke).
1011
1016
  - Not changed: the `halved` PR-conflict split still re-batches its members, and
1012
1017
  aggregate-suite evictions (`evictMembers`, post-landing, member branch already deleted)
1013
- still requeue full-cycle — now on the batch profile.
1018
+ still requeue full-cycle — now on the batch profile. Both, plus an engine-set requeue base,
1019
+ are tracked in #840.
1014
1020
 
1015
1021
  State schema 1.24.0: `IssueStatus` gains `handed-back`; `EvictionRecord` gains optional
1016
1022
  `kind`/`branch`, `FailureEvidence` optional `branch`.
@@ -1052,13 +1058,50 @@ State schema 1.24.0: `IssueStatus` gains `handed-back`; `EvictionRecord` gains o
1052
1058
  the batch. A spawn that finds the count already spent (a crash between the count and the
1053
1059
  block) refuses instead of spawning. Before #832, b-20260924-04's tail was respawned four
1054
1060
  times at the strong tier (~375k tokens) until an operator stopped the batch.
1055
- - **Clearing these blocks.** `no-landed-members`, `members-mismatch:*`, `tail-blocked:*` and
1056
- `respawn-cap:tail` have no `sched resume --batch` recheck. `sched status` names the
1057
- evidence (the anchor's blocked milestone, or the tail log) and the exits: fix the cause
1058
- on the integration branch and open its PR by hand (`gh pr create --head <branch> --base
1059
- <base>`; once it merges, `reconcileStaleBlockedBatches` settles the batch), or `sched
1060
- abandon --batch <id>` (requeues the landed members full-cycle; for `no-landed-members`
1061
- there is nothing to lose).
1061
+ - **Clearing these blocks.** `sched status` names the evidence (the anchor's blocked
1062
+ milestone, or the tail log) and the exits. For `members-mismatch:*`, `tail-blocked:*` and
1063
+ `respawn-cap:tail`: fix the cause, then `sched resume --batch <id>` (#822, below), or open
1064
+ the integration branch's PR by hand (`gh pr create --head <branch> --base <base>`; once it
1065
+ merges, `reconcileStaleBlockedBatches` settles the batch), or `sched abandon --batch <id>`
1066
+ (requeues the landed members full-cycle). `no-landed-members` has nothing to resume:
1067
+ abandon it.
1068
+
1069
+ ### Resuming a batch blocked over landed work (#822)
1070
+
1071
+ `sched resume --batch <id>` on a batch blocked `dissolve-refused:*` (#810) or on one of #832's
1072
+ tail blocks (`tail-blocked:*`, `members-mismatch:*`, `respawn-cap:tail`) —
1073
+ `LANDED_RESUMABLE_BLOCK_PREFIXES` — calls `resumeLandedBatch`: `blocked → validating`, with
1074
+ `blocked_reason`/`agent_exits` cleared and `executing_member` pinned to the last member (so
1075
+ `runValidate` never re-admits a member the dissolve released). The engine's next tick
1076
+ re-runs the batch gate and then the tail over the landed members only; a gate still red
1077
+ re-blocks it once. It journals `batch-resumed` (naming any tail exits it cleared). Refused,
1078
+ with the reason, when no member is landed, the batch still holds a slot, it has no
1079
+ integration worktree/branch/anchor, a PR is already recorded against it (let the stale-blocked
1080
+ reconcile settle it), or the tail's own members check would refuse the same landed set again
1081
+ (`tailMembersRefusal`). The CLI also refuses when the integration worktree is gone from disk
1082
+ or a profiled batch's dispatch profile no longer resolves (restore it first).
1083
+ `gate-inconclusive:*` keeps its own recheck (`resumeBlockedGate`).
1084
+
1085
+ ### Wrong-procedure members are re-prompted once (#822)
1086
+
1087
+ A member whose dispatch posts a full-cycle-line milestone (`gate` … `report`) carrying neither
1088
+ `batch=` nor `mode=slot` ran the WRONG PROCEDURE (imboard #4174 ran full-cycle as a
1089
+ member: `review done next=ship`) — `isWrongProcedureMilestone`, fenced to this dispatch. A
1090
+ hand-back shape (`blocked`, `review partial`) never counts: it keeps its own path. Both
1091
+ member rails then:
1092
+
1093
+ - stop a live agent and wait for it to be gone before anything else (a dying full-cycle run
1094
+ could still push, post or open a PR in that worktree);
1095
+ - if the milestone shows the run already SHIPPED (a `ship`/`report` phase or a `pr=` key —
1096
+ `wrongProcedureShippedPr`), evict it at once `wrong-procedure-shipped`, naming the stray PR
1097
+ (`stray_pr`) for the operator to close or reconcile — a re-prompt cannot undo a PR;
1098
+ - otherwise, the first time, record it in `BatchEntry.reprompted_members`
1099
+ (`{ issue, milestone_at, reprompted_at }`, schema 1.27.0) and journal `member-reprompted`:
1100
+ it respawns in place, in the same worktree, with `wrongProcedureDirective` appended to its
1101
+ prompt (engine values only — no milestone text);
1102
+ - a later wrong-procedure milestone posted strictly after `reprompted_at` (no skew tolerance:
1103
+ the killed first dispatch's milestones would otherwise still count inside the 60s fence)
1104
+ evicts it `wrong-procedure` (parked `evicted`, branch kept, #810).
1062
1105
 
1063
1106
  ## API surface
1064
1107
 
@@ -1225,6 +1268,11 @@ import {
1225
1268
  type CapOutcome, // ok | task-failed | automation-broken | capability-unavailable
1226
1269
  type CapabilityGateResult, // {outcome: CapOutcome, outputTail?, reason?} — runCapability's return shape (#583)
1227
1270
  resumeBlockedGate, // #583: sched resume --batch <id> — re-run the gate that blocked a batch
1271
+ resumeLandedBatch, // #822: sched resume --batch <id> over landed work — blocked → validating
1272
+ isLandedResumableBlock, LANDED_RESUMABLE_BLOCK_PREFIXES, // #822: which blocks resumeLandedBatch accepts
1273
+ isWrongProcedureMilestone, wrongProcedureShippedPr, // #822: wrong-procedure detection; did the stray run already ship?
1274
+ wrongProcedureDirective, WRONG_PROCEDURE_MARKER, // #822: the re-prompt text and its fixed opening
1275
+ type RepromptRecord, // #822: BatchEntry.reprompted_members ({ issue, milestone_at, reprompted_at })
1228
1276
  buildMemberPrompt, buildBatchTailPrompt, buildBatchReportPrompt, // #523 prompt builders (#677: member carries {issue}/{batch}/{worktree}/{integration_branch})
1229
1277
  memberBranchFor, // #677: the member branch name, `batch/<id>-m<n>-<issue>` — one definition, every recovery surface derives from it
1230
1278
  DEFAULT_MEMBER_PROMPT_TEMPLATE, DEFAULT_BATCH_TAIL_PROMPT_TEMPLATE,
@@ -1366,7 +1414,7 @@ after-the-fact recovery, not a missing-data bug.
1366
1414
  queue data.
1367
1415
  - **Schema**: state/config files from #460 (schema 1.0.0), #464 (1.1.0), #468 (1.2.0),
1368
1416
  #472 (1.3.0), #500 (1.4.0), #505 (1.5.0), #504 (1.6.0), #523 (1.7.0) and #524 (1.8.0)
1369
- load and migrate to the current schema (1.26.0 — 1.24.0 was #810: no backfill,
1417
+ load and migrate to the current schema (1.27.0 — 1.24.0 was #810: no backfill,
1370
1418
  `kind`/`branch` optional, absent = an `evicted` record with no branch) automatically
1371
1419
  (slot `branch`/`last_head`/`pid_start`, slot `role` (inferred from the
1372
1420
  unit's queue entry, with the persisted `phase` as a fallback — #500), entry
@@ -1387,6 +1435,8 @@ after-the-fact recovery, not a missing-data bug.
1387
1435
  backfill to `null`/`null`/`0` — #789, schema 1.25.0).
1388
1436
  Schema 1.26.0 (#832): `BatchEntry` gains `agent_exits` (the per-phase tail/report
1389
1437
  respawn counter); `null` is backfilled on load.
1438
+ Schema 1.27.0 (#822): `BatchEntry` gains `reprompted_members`
1439
+ (`{ issue, milestone_at, reprompted_at }` records; `[]` backfilled).
1390
1440
  - **`max_slots`** bounds live units (`assigned | running | recovering`); dependency
1391
1441
  edges gate readiness — an issue with an unmerged dependency, and a batch behind an
1392
1442
  unmerged batch, are never runnable.
@@ -68,8 +68,10 @@ import { type Journal } from './journal';
68
68
  import type { SchedStore } from './persist';
69
69
  import type { ExecFn } from './project';
70
70
  import { type BatchSuiteContext, type SuiteResult } from './recovery';
71
+ import { tailMembersRefusal } from './state';
71
72
  import { type FsExists } from './teardown';
72
73
  import type { BatchEntry, CapabilityGateResult, MemberDispatchMode, MemberExitKind, SchedConfig, SchedState } from './types';
74
+ export { tailMembersRefusal };
73
75
  export type { CapOutcome } from './types';
74
76
  /** Everything batch dispatch needs from the outside world. */
75
77
  export interface BatchDispatchDeps {
@@ -172,20 +174,6 @@ export interface MemberFailure {
172
174
  * scheduler lands it there.
173
175
  */
174
176
  export declare function memberBranchFor(batchId: string, memberIndex: number, issue: number): string;
175
- /**
176
- * #832: why the tail must NOT be dispatched over `landed`, or null when it may.
177
- * The deterministic half of the check the tail agent itself makes before
178
- * integrating (the members it is told about and the members it derives from
179
- * the integration branch's boundary commits must agree) — made here for free
180
- * instead of by a strong-tier agent that then exits `members-mismatch`.
181
- * `batch.ranges` is the recorded boundary attribution (recomputed at every
182
- * landing); when it is empty nothing was attributed, so there is nothing to
183
- * cross-check. Invariant this relies on: `ranges` names only landed members —
184
- * every exit path that reverts a member also drops its range
185
- * (`recovery.ts`'s partial dissolve); a path that re-derived `ranges` from
186
- * `git log` after a revert would make this refuse a healthy batch.
187
- */
188
- export declare function tailMembersRefusal(batch: BatchEntry, landed: readonly number[]): string | null;
189
177
  /**
190
178
  * Evict the current member and either dissolve, or continue the batch via
191
179
  * `advanceMemberOrValidate` — the shared tail of both member-failure rails
@@ -1 +1 @@
1
- {"version":3,"file":"batch-dispatch.d.ts","sourceRoot":"","sources":["../src/batch-dispatch.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"batch-dispatch.d.ts","sourceRoot":"","sources":["../src/batch-dispatch.ts"],"names":[],"mappings":"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"}
@@ -96,9 +96,8 @@ var __importStar = (this && this.__importStar) || (function () {
96
96
  };
97
97
  })();
98
98
  Object.defineProperty(exports, "__esModule", { value: true });
99
- exports.MAX_BATCH_AGENT_RESPAWNS = void 0;
99
+ exports.MAX_BATCH_AGENT_RESPAWNS = exports.tailMembersRefusal = void 0;
100
100
  exports.memberBranchFor = memberBranchFor;
101
- exports.tailMembersRefusal = tailMembersRefusal;
102
101
  exports.evictMemberAndContinue = evictMemberAndContinue;
103
102
  exports.resumeBlockedGate = resumeBlockedGate;
104
103
  exports.reconcileStaleBlockedBatches = reconcileStaleBlockedBatches;
@@ -119,6 +118,7 @@ const recovery_1 = require("./recovery");
119
118
  const run_log_1 = require("./run-log");
120
119
  const scheduler_1 = require("./scheduler");
121
120
  const state_1 = require("./state");
121
+ Object.defineProperty(exports, "tailMembersRefusal", { enumerable: true, get: function () { return state_1.tailMembersRefusal; } });
122
122
  const teardown_1 = require("./teardown");
123
123
  const types_1 = require("./types");
124
124
  function emptyResult() {
@@ -162,6 +162,12 @@ function sanitizeUntrustedText(value) {
162
162
  function journalEvent(deps, event, unitId, extra = {}) {
163
163
  deps.journal.append((0, journal_1.unitEvent)(event, unitId, extra), deps.now());
164
164
  }
165
+ /** Stop the slot's agent if it is still alive (pid-start-safe: a reused pid is never signalled). */
166
+ function killLiveAgent(deps, slot) {
167
+ if (slot.pid !== null && deps.spawnDeps.isAlive(slot.pid, slot.pid_start ?? undefined)) {
168
+ deps.spawnDeps.kill(slot.pid, slot.pid_start ?? undefined);
169
+ }
170
+ }
165
171
  /**
166
172
  * Read a dead batch dispatch's own log and classify it (#629 AC3) — tail,
167
173
  * member, fix and report all reach a `dead` agent the same way (`pid` no
@@ -1016,7 +1022,11 @@ function spawnMemberAgent(deps, dispatch, state, slot, batchId, now, result, tar
1016
1022
  // #677: the prompt carries THIS member's own worktree and the integration
1017
1023
  // branch it was cut from — `member-cycle`'s `worktree`/`integration_branch`
1018
1024
  // inputs. The shared batch worktree is no longer a member surface at all.
1019
- const prompt = (0, dispatch_1.buildMemberPrompt)(dispatch.memberPrompt, memberIssue, batchId, target.member.worktree, batch.branch, review);
1025
+ const prompt = (0, dispatch_1.buildMemberPrompt)(dispatch.memberPrompt, memberIssue, batchId, target.member.worktree, batch.branch, review) +
1026
+ // #822: a member respawned after running the wrong procedure.
1027
+ (batch.reprompted_members.some((r) => r.issue === memberIssue)
1028
+ ? (0, dispatch_1.wrongProcedureDirective)(memberIssue, batchId)
1029
+ : '');
1020
1030
  const logFile = (0, dispatch_1.batchMemberLogPath)(deps.store.runsDir, batchId, target.index, memberIssue);
1021
1031
  // #629: captured BEFORE spawning, mirroring `engine.ts`'s own
1022
1032
  // `spawnAndRecord` — the log is per-role and append-mode, so the size at
@@ -1311,36 +1321,6 @@ function spawnMemberContinuation(deps, config, dispatch, batchId, now, result) {
1311
1321
  }
1312
1322
  claimAndSpawn(deps, config, batchId, 'member', now, (nextState, slot) => spawnMember(deps, dispatch, nextState, slot, batchId, now, result, member));
1313
1323
  }
1314
- /**
1315
- * #832: why the tail must NOT be dispatched over `landed`, or null when it may.
1316
- * The deterministic half of the check the tail agent itself makes before
1317
- * integrating (the members it is told about and the members it derives from
1318
- * the integration branch's boundary commits must agree) — made here for free
1319
- * instead of by a strong-tier agent that then exits `members-mismatch`.
1320
- * `batch.ranges` is the recorded boundary attribution (recomputed at every
1321
- * landing); when it is empty nothing was attributed, so there is nothing to
1322
- * cross-check. Invariant this relies on: `ranges` names only landed members —
1323
- * every exit path that reverts a member also drops its range
1324
- * (`recovery.ts`'s partial dissolve); a path that re-derived `ranges` from
1325
- * `git log` after a revert would make this refuse a healthy batch.
1326
- */
1327
- function tailMembersRefusal(batch, landed) {
1328
- if (landed.length === 0)
1329
- return 'no-landed-members';
1330
- if (batch.ranges.length === 0)
1331
- return null;
1332
- const attributed = new Set(batch.ranges.map((r) => r.issue));
1333
- const missing = landed.filter((issue) => !attributed.has(issue));
1334
- if (missing.length > 0)
1335
- return `members-mismatch:no-boundary-commit-${missing.join(',')}`;
1336
- // The tail derives members from the boundary commits too, and refuses when
1337
- // the two lists disagree in EITHER direction (review-issue Aggregate Step 1).
1338
- const landedSet = new Set(landed);
1339
- const unlanded = [...attributed].filter((issue) => !landedSet.has(issue));
1340
- return unlanded.length > 0
1341
- ? `members-mismatch:unlanded-boundary-commit-${unlanded.join(',')}`
1342
- : null;
1343
- }
1344
1324
  /**
1345
1325
  * #832: the one pre-dispatch verdict for a tail — checked before a slot is
1346
1326
  * claimed and again under the claim's lock. A tail whose respawns are spent
@@ -1353,7 +1333,7 @@ function tailDispatchRefusal(state, batch) {
1353
1333
  return null;
1354
1334
  if (agentRespawnsSpent(batch, 'tail'))
1355
1335
  return 'respawn-cap:tail';
1356
- return tailMembersRefusal(batch, (0, state_1.validatedMembersOf)(state, batch));
1336
+ return (0, state_1.tailMembersRefusal)(batch, (0, state_1.validatedMembersOf)(state, batch));
1357
1337
  }
1358
1338
  /**
1359
1339
  * Tail/report/fix dispatches (this function, `spawnReportAgent`,
@@ -2091,7 +2071,7 @@ function resumeBlockedGate(deps, config, dispatch, batchId, now) {
2091
2071
  // `suite-unreadable`) — name the actual reason and the operator path
2092
2072
  // instead of a bare illegal-transition error that reads like a bug.
2093
2073
  if (batch.status === 'blocked') {
2094
- throw new types_1.SchedNotFoundError(`Batch ${batchId} is blocked on '${batch.blocked_reason ?? '?'}', which has no gate recheck: inspect the batch worktree/state named in its journal, then \`sched abandon --batch ${batchId}\` if it is a dead end`);
2074
+ throw new types_1.SchedNotFoundError(`Batch ${batchId} is blocked on '${batch.blocked_reason ?? '?'}', which has no gate recheck (a batch blocked over landed work — dissolve-refused / tail-blocked / members-mismatch / respawn-cap:tail — resumes via \`sched resume --batch\`, #822): inspect the batch worktree/state named in its journal, then \`sched abandon --batch ${batchId}\` if it is a dead end`);
2095
2075
  }
2096
2076
  throw new types_1.IllegalTransitionError('batch', batch.status, 'executing');
2097
2077
  }
@@ -2367,8 +2347,113 @@ function readMemberDispatch(deps, batchId, memberIssue, slot, now) {
2367
2347
  milestone,
2368
2348
  dead,
2369
2349
  blockedNow: (0, groundtruth_1.isMemberBlocked)(milestone, slot.spawned_at),
2350
+ wrongProcedure: (0, groundtruth_1.isWrongProcedureMilestone)(milestone, slot.spawned_at),
2370
2351
  };
2371
2352
  }
2353
+ /**
2354
+ * #822: whether a wrong-procedure read counts against THIS dispatch of
2355
+ * `memberIssue`. After a re-prompt, only a milestone posted strictly after
2356
+ * the re-prompt does: the killed first dispatch's milestones still sit
2357
+ * inside the respawn's 60s fence tolerance and would otherwise evict the
2358
+ * member the moment it respawns. No tolerance here — the engine and the
2359
+ * member's CLI stamp times on the same host.
2360
+ */
2361
+ function wrongProcedureCounts(batch, memberIssue, read) {
2362
+ if (!read.wrongProcedure || read.milestone === null)
2363
+ return false;
2364
+ const prior = batch.reprompted_members.find((r) => r.issue === memberIssue);
2365
+ if (prior === undefined)
2366
+ return true;
2367
+ const at = Date.parse(read.milestone.at);
2368
+ const repromptedAt = Date.parse(prior.reprompted_at);
2369
+ return Number.isNaN(at) || Number.isNaN(repromptedAt) ? false : at > repromptedAt;
2370
+ }
2371
+ /**
2372
+ * #822 (#810 proposal 3): the member's dispatch ran the wrong procedure (a
2373
+ * full-cycle-shaped trail — imboard #4174 ran full-cycle as a batch member).
2374
+ * That is a dispatch mistake, not the member's work failing, so the first
2375
+ * time it is RE-PROMPTED: a live agent is stopped (a full-cycle run left going
2376
+ * would open its own PR), the slot is released, and the member is recorded in
2377
+ * `reprompted_members` — the wedge arm (serial) or `spawnParallelMembers`
2378
+ * (parallel) respawns it in place, in the same worktree, with
2379
+ * `wrongProcedureDirective` appended. Returns `'evict'` when the member was
2380
+ * already re-prompted once: the caller evicts it `wrong-procedure`.
2381
+ */
2382
+ function repromptWrongProcedure(deps, batchId, memberIssue, milestoneAt, slot, release, now) {
2383
+ // Never respawn into (or release) a worktree the old agent is still in: a
2384
+ // dying full-cycle run could still push, post, or open a PR. Signal it and
2385
+ // decide on a later tick, once it is gone — and so the re-prompt's
2386
+ // `reprompted_at` fence postdates everything it could still post.
2387
+ if (slot.pid !== null && deps.spawnDeps.isAlive(slot.pid, slot.pid_start ?? undefined)) {
2388
+ deps.spawnDeps.kill(slot.pid, slot.pid_start ?? undefined);
2389
+ return 'waiting';
2390
+ }
2391
+ const outcome = deps.store.withLock((s) => {
2392
+ const released = release(s);
2393
+ const b = (0, state_1.findBatch)(released, batchId);
2394
+ if (!b || b.reprompted_members.some((r) => r.issue === memberIssue)) {
2395
+ return { state: released, result: 'evict' };
2396
+ }
2397
+ return {
2398
+ state: (0, state_1.patchBatch)(released, batchId, {
2399
+ reprompted_members: [
2400
+ ...b.reprompted_members,
2401
+ { issue: memberIssue, milestone_at: milestoneAt, reprompted_at: now.toISOString() },
2402
+ ],
2403
+ }, now),
2404
+ result: 'reprompted',
2405
+ };
2406
+ });
2407
+ if (outcome === 'reprompted') {
2408
+ journalEvent(deps, 'member-reprompted', unit(batchId), {
2409
+ issue: memberIssue,
2410
+ reason: 'wrong-procedure',
2411
+ milestone_at: milestoneAt,
2412
+ detail: `${WRONG_PROCEDURE_SHAPE} — respawning it once in place with a member-cycle directive`,
2413
+ });
2414
+ }
2415
+ return outcome;
2416
+ }
2417
+ /** #822: the evidence line every wrong-procedure journal/eviction names. */
2418
+ const WRONG_PROCEDURE_SHAPE = 'member posted a full-cycle-shaped milestone (no batch=/mode=slot key)';
2419
+ /** #822: the eviction a member earns for running the wrong procedure after its one re-prompt. */
2420
+ function wrongProcedureFailure(lastTool, shippedPr = null) {
2421
+ return {
2422
+ reason: shippedPr === null ? 'wrong-procedure' : 'wrong-procedure-shipped',
2423
+ detail: shippedPr === null
2424
+ ? `${WRONG_PROCEDURE_SHAPE} again after its one re-prompt`
2425
+ : `${WRONG_PROCEDURE_SHAPE} and its full-cycle run already reached ship (PR ${shippedPr === 'unknown' ? 'not named' : `#${shippedPr}`}) — evicted without a re-prompt; close or reconcile that PR by hand`,
2426
+ kind: 'evicted',
2427
+ extraKv: {
2428
+ ...(lastTool ? { last_tool: lastTool } : {}),
2429
+ ...(typeof shippedPr === 'number' ? { stray_pr: String(shippedPr) } : {}),
2430
+ },
2431
+ };
2432
+ }
2433
+ /**
2434
+ * #822: the one wrong-procedure decision shared by both member rails —
2435
+ * `'waiting'` (the old agent is still dying), `'reprompted'`, or the
2436
+ * eviction failure to apply. A run that already shipped is evicted without
2437
+ * a re-prompt.
2438
+ */
2439
+ function decideWrongProcedure(deps, batchId, memberIssue, milestone, slot, release, lastTool, now) {
2440
+ const shipped = (0, groundtruth_1.wrongProcedureShippedPr)(milestone);
2441
+ if (shipped !== null) {
2442
+ if (slot.pid !== null && deps.spawnDeps.isAlive(slot.pid, slot.pid_start ?? undefined)) {
2443
+ deps.spawnDeps.kill(slot.pid, slot.pid_start ?? undefined);
2444
+ return 'waiting';
2445
+ }
2446
+ const tool = lastTool();
2447
+ deps.store.withLock((s) => ({ state: release(s), result: undefined }));
2448
+ return wrongProcedureFailure(tool, shipped);
2449
+ }
2450
+ const verdict = repromptWrongProcedure(deps, batchId, memberIssue, milestone.at, slot, release, now);
2451
+ if (verdict === 'waiting')
2452
+ return verdict;
2453
+ // The finished dispatch's run-log entry, exactly once (never while waiting).
2454
+ const tool = lastTool();
2455
+ return verdict === 'reprompted' ? verdict : wrongProcedureFailure(tool);
2456
+ }
2372
2457
  /**
2373
2458
  * The eviction a blocked-or-dead member dispatch earns (shared by both rails).
2374
2459
  * #605: only a milestone THIS dispatch posted may name the reason. A dead
@@ -2438,6 +2523,16 @@ function reconcileMemberSlot(deps, config, dispatch, batchId, slot, now, result)
2438
2523
  completeMemberGate(deps, config, dispatch, batchId, batch, memberIssue, now, result);
2439
2524
  return;
2440
2525
  }
2526
+ // #822: a wrong-procedure trail is re-prompted once, then evicted — decided
2527
+ // before the dead/blocked rail, which would otherwise evict it as an
2528
+ // unverified exit at the first sight.
2529
+ if (wrongProcedureCounts(batch, memberIssue, read) && read.milestone !== null) {
2530
+ const decision = decideWrongProcedure(deps, batchId, memberIssue, read.milestone, slot, (s) => releaseSlot(s, batchId, now), () => recordMemberRunLog(deps, dispatch, state0, batchId, batch.executing_member, memberIssue, slot, now), now);
2531
+ if (typeof decision === 'object') {
2532
+ evictMemberAndContinue(deps, config, dispatch, batchId, batch, memberIssue, decision, now, result);
2533
+ }
2534
+ return;
2535
+ }
2441
2536
  if (read.dead &&
2442
2537
  !read.blockedNow &&
2443
2538
  handleDeadDispatchApiError(deps, batchId, (0, dispatch_1.batchMemberLogPath)(deps.store.runsDir, batchId, batch.executing_member, memberIssue), slot.log_offset_at_spawn ?? 0, now)) {
@@ -2778,8 +2873,8 @@ function reconcileTailSlot(deps, config, batchId, slot, now, result) {
2778
2873
  ? null
2779
2874
  : (0, groundtruth_1.batchPhaseBlockedReason)(milestone, ['batch-review', 'batch-ship'], slot.spawned_at);
2780
2875
  if (handedBack !== null) {
2781
- if (slot.pid !== null && !dead)
2782
- deps.spawnDeps.kill(slot.pid, slot.pid_start ?? undefined);
2876
+ if (!dead)
2877
+ killLiveAgent(deps, slot);
2783
2878
  blockOnTailHandBack(deps, config, batch, `tail-blocked:${handedBack}`, now, result);
2784
2879
  return;
2785
2880
  }
@@ -3904,9 +3999,7 @@ function reconcileParallelMemberSlot(deps, config, dispatch, batchId, run, slot,
3904
3999
  // into that tree with its pid no longer tracked anywhere (so neither a
3905
4000
  // dissolve nor `sched stop --batch` could stop it). Its work is done —
3906
4001
  // stop it first.
3907
- if (slot.pid !== null && deps.spawnDeps.isAlive(slot.pid, slot.pid_start ?? undefined)) {
3908
- deps.spawnDeps.kill(slot.pid, slot.pid_start ?? undefined);
3909
- }
4002
+ killLiveAgent(deps, slot);
3910
4003
  journalEvent(deps, 'external-advance', unit(batchId), {
3911
4004
  issue: run.issue,
3912
4005
  detail: 'member review done',
@@ -3929,6 +4022,17 @@ function reconcileParallelMemberSlot(deps, config, dispatch, batchId, run, slot,
3929
4022
  result.completed.push(memberUnit);
3930
4023
  return;
3931
4024
  }
4025
+ // #822: re-prompt once, then evict — see the serial rail.
4026
+ const parallelBatch = (0, state_1.findBatch)(state0, batchId);
4027
+ if (parallelBatch &&
4028
+ wrongProcedureCounts(parallelBatch, run.issue, read) &&
4029
+ read.milestone !== null) {
4030
+ const decision = decideWrongProcedure(deps, batchId, run.issue, read.milestone, slot, release, () => recordMemberRunLog(deps, dispatch, state0, batchId, run.index, run.issue, slot, now), now);
4031
+ if (typeof decision === 'object') {
4032
+ evictParallelMember(deps, config, batchId, run, decision, now, result);
4033
+ }
4034
+ return;
4035
+ }
3932
4036
  if (read.dead &&
3933
4037
  !read.blockedNow &&
3934
4038
  handleDeadDispatchApiError(deps, batchId, (0, dispatch_1.batchMemberLogPath)(deps.store.runsDir, batchId, run.index, run.issue), slot.log_offset_at_spawn ?? 0, now, { releaseWith: release })) {
@@ -4239,9 +4343,7 @@ function stopMemberAgent(deps, batchId, issue, now) {
4239
4343
  const slot = (0, state_1.slotForBatchMember)(deps.store.load(), batchId, issue);
4240
4344
  if (slot === undefined)
4241
4345
  return;
4242
- if (slot.pid !== null && deps.spawnDeps.isAlive(slot.pid, slot.pid_start ?? undefined)) {
4243
- deps.spawnDeps.kill(slot.pid, slot.pid_start ?? undefined);
4244
- }
4346
+ killLiveAgent(deps, slot);
4245
4347
  deps.store.withLock((s) => ({
4246
4348
  state: (0, state_1.releaseBatchMemberSlot)(s, batchId, issue, now),
4247
4349
  result: undefined,