agent-dealer 1.2.8 → 1.2.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 (60) hide show
  1. package/bundle/server/dist/adapters/github.js +93 -4
  2. package/bundle/server/dist/adapters/github.test.js +96 -1
  3. package/bundle/server/dist/adapters/worktree-deps.js +96 -0
  4. package/bundle/server/dist/adapters/worktree-deps.test.js +144 -0
  5. package/bundle/server/dist/coordinator/auto-merge.js +205 -1
  6. package/bundle/server/dist/coordinator/checks-wait-config.js +19 -0
  7. package/bundle/server/dist/coordinator/commands.js +341 -18
  8. package/bundle/server/dist/coordinator/commands.test.js +47 -0
  9. package/bundle/server/dist/coordinator/developer-effect.js +237 -3
  10. package/bundle/server/dist/coordinator/developer-effect.test.js +460 -3
  11. package/bundle/server/dist/coordinator/human-resolution.js +19 -0
  12. package/bundle/server/dist/coordinator/human-resolution.test.js +33 -0
  13. package/bundle/server/dist/coordinator/merge-conflict-sync.js +100 -12
  14. package/bundle/server/dist/coordinator/merge-conflict-sync.test.js +190 -0
  15. package/bundle/server/dist/coordinator/muse-spawn.js +22 -4
  16. package/bundle/server/dist/coordinator/muse-spawn.test.js +166 -10
  17. package/bundle/server/dist/coordinator/operator-criteria.js +168 -0
  18. package/bundle/server/dist/coordinator/operator-criteria.test.js +78 -0
  19. package/bundle/server/dist/coordinator/operator-verification.integration.test.js +347 -0
  20. package/bundle/server/dist/coordinator/playbook-feedback.js +3 -0
  21. package/bundle/server/dist/coordinator/projection.js +10 -4
  22. package/bundle/server/dist/coordinator/projection.test.js +13 -0
  23. package/bundle/server/dist/coordinator/prompts.js +134 -1
  24. package/bundle/server/dist/coordinator/prompts.test.js +148 -0
  25. package/bundle/server/dist/coordinator/recovery.js +57 -3
  26. package/bundle/server/dist/coordinator/recovery.test.js +67 -8
  27. package/bundle/server/dist/coordinator/repair-cycle.integration.test.js +1 -0
  28. package/bundle/server/dist/coordinator/reviewer-effect.js +6 -0
  29. package/bundle/server/dist/coordinator/routing.js +55 -6
  30. package/bundle/server/dist/coordinator/routing.test.js +62 -15
  31. package/bundle/server/dist/coordinator/session-timeouts.js +22 -0
  32. package/bundle/server/dist/coordinator/usage-cap-defer.js +61 -0
  33. package/bundle/server/dist/db/ci-attempts-migration.test.js +70 -0
  34. package/bundle/server/dist/db/index.js +13 -0
  35. package/bundle/server/dist/db/schema.sql +2 -0
  36. package/bundle/server/dist/repository/issues.js +39 -3
  37. package/bundle/server/dist/repository/work-items.js +7 -0
  38. package/bundle/server/dist/runners/muse-code-args.js +25 -0
  39. package/bundle/server/dist/runners/muse-code-args.test.js +30 -0
  40. package/bundle/server/dist/runners/muse-code-jsonl.js +61 -0
  41. package/bundle/server/dist/runners/muse-code.test.js +13 -1
  42. package/bundle/server/dist/runners/spawn-cli.js +93 -20
  43. package/bundle/server/dist/runners/spawn-cli.test.js +38 -0
  44. package/bundle/server/package.json +2 -2
  45. package/bundle/server/static-ui/assets/index-CU6fNntg.js +64 -0
  46. package/bundle/server/static-ui/index.html +1 -1
  47. package/bundle/shared/dist/human-actions.d.ts +4 -4
  48. package/bundle/shared/dist/human-actions.js +8 -0
  49. package/bundle/shared/dist/issues.d.ts +30 -0
  50. package/bundle/shared/dist/issues.js +14 -0
  51. package/bundle/shared/dist/issues.test.js +2 -0
  52. package/bundle/shared/dist/workflow.d.ts +4 -4
  53. package/bundle/shared/dist/workflow.js +6 -0
  54. package/bundle/shared/dist/workflow.test.js +1 -0
  55. package/bundle/shared/package.json +1 -1
  56. package/dist/action.js +4 -1
  57. package/dist/action.test.js +42 -0
  58. package/dist/index.js +1 -1
  59. package/package.json +1 -1
  60. package/bundle/server/static-ui/assets/index-B6SVCzMR.js +0 -64
@@ -27,14 +27,27 @@ import { classifyIssueRepo } from "../adapters/managed-repo.js";
27
27
  import { getDb } from "../db/index.js";
28
28
  import { getIssue, listIssues, transitionIssue } from "../repository/issues.js";
29
29
  import { appendWorkflowEvent, completeWorkflowInstance, getActiveWorkflowInstance, } from "../repository/workflow-events.js";
30
- import { createHumanAction, findOpenHumanAction, resolveHumanAction, } from "../repository/human-actions.js";
30
+ import { createHumanAction, findOpenHumanAction, findOpenHumanActionByRequestId, resolveHumanAction, } from "../repository/human-actions.js";
31
31
  import { MERGE_FAILURE_EVIDENCE_KEY, MERGE_FAILURE_RESPONSE_OPTIONS } from "./human-resolution.js";
32
32
  import { isMergeConflictFailure, runMergeConflictSync } from "./merge-conflict-sync.js";
33
+ import { OPERATOR_VERIFICATION_RESPONSE_OPTIONS, formatOperatorCriteria, getOperatorCriteriaForIssue, hasOperatorVerificationForHead, operatorVerificationRequestId, } from "./operator-criteria.js";
33
34
  const run = promisify(execFile);
34
35
  /** Bound each `gh` shell-out so a hang cannot freeze the coordinator process. */
35
36
  export const GH_MERGE_TIMEOUT_MS = 20_000;
36
37
  /** Must match projection.ts's auto_merge currentIntent — recovery keys off this string. */
37
38
  export const AUTO_MERGE_INTENT = "Auto-merging approved PR";
39
+ /**
40
+ * NOT-314: the issue intent while an `[operator]` gate holds the merge. Named
41
+ * verbatim by the ticket: the PR stays unmerged in `needs_human` under this intent
42
+ * until the human records a result or waives the criterion.
43
+ */
44
+ export const OPERATOR_VERIFICATION_INTENT = "Operator verification required before merge";
45
+ /**
46
+ * NOT-314 fail-closed: the stable request id for the no-head escalation, so
47
+ * repeat finalizes dedupe onto the one open `policy_escalation` instead of
48
+ * stacking a new one per tick.
49
+ */
50
+ export const OPERATOR_VERIFICATION_NO_HEAD_REQUEST_ID = "operator-verification-blocked:no-head";
38
51
  const ALREADY_MERGED = /already (been )?merged|pull request is not mergeable:.*merged/i;
39
52
  /**
40
53
  * Map issue.repo (portable identity or legacy local path) to a real filesystem cwd for
@@ -184,6 +197,15 @@ async function finalizeAutoMergeOnce(issueId) {
184
197
  if (issue.prNumber == null) {
185
198
  return escalateMergeFailure(issue, instance.id, "Reviewer approved but the issue has no PR number to merge.");
186
199
  }
200
+ // NOT-314 seam: this finalize is the single path from reviewer-approve to the
201
+ // `gh` merge — auto-merge-on routes here from applyCompletion, auto-merge-off
202
+ // arrives via final_review:merge / policy_escalation:retry_merge, and crash
203
+ // recovery re-enters through recoverStrandedAutoMerges. An `[operator]`
204
+ // criterion in the frozen snapshot blocks here (PR unmerged, needs_human)
205
+ // until a human records the result for the current head.
206
+ const operatorBlock = gateOperatorVerification(issue.id);
207
+ if (operatorBlock)
208
+ return operatorBlock;
187
209
  const resolvedCwd = resolveAutoMergeCwd(issue.repo);
188
210
  if (!resolvedCwd.ok) {
189
211
  return escalateMergeFailure(issue, instance.id, `Auto-merge failed: ${resolvedCwd.reason}`);
@@ -287,6 +309,188 @@ async function finalizeAutoMergeOnce(issueId) {
287
309
  };
288
310
  })();
289
311
  }
312
+ /**
313
+ * NOT-314: the `[operator]` merge gate. After reviewer approve and before the
314
+ * `gh` merge: when the frozen snapshot holds operator criteria and no result is
315
+ * recorded for the current head (a `verified` artifact or a
316
+ * `operator_verification.waived` event — both head-pinned), the PR stays
317
+ * unmerged and the issue parks in `needs_human` with an `operator_verification`
318
+ * action listing each criterion and its command.
319
+ *
320
+ * Returns null when the merge may proceed (no operator criteria, a recorded
321
+ * result for this head, or no workflow to gate). Idempotent: a second finalize
322
+ * for the same head reuses the open action instead of raising another.
323
+ *
324
+ * Fail-closed: when operator criteria exist, every non-proceed path returns a
325
+ * result (never null), so a missing head SHA or an unexpected status blocks the
326
+ * `gh` merge instead of letting it through.
327
+ */
328
+ export function gateOperatorVerification(issueId) {
329
+ return getDb().transaction(() => {
330
+ const current = getIssue(issueId);
331
+ if (!current)
332
+ return null;
333
+ const active = getActiveWorkflowInstance(issueId);
334
+ if (!active)
335
+ return null;
336
+ const criteria = getOperatorCriteriaForIssue(current);
337
+ if (criteria.length === 0)
338
+ return null;
339
+ const head = current.headSha;
340
+ if (head && hasOperatorVerificationForHead(issueId, head))
341
+ return null;
342
+ if (current.status === "done")
343
+ return null;
344
+ // Only the auto-merge park (or an already-gated issue) is gated — any other
345
+ // status is a racer or a non-parked issue; do not invent an action there,
346
+ // but do not let the merge through either — park the finalize as a no-op.
347
+ if (current.status !== "final_review" && current.status !== "needs_human") {
348
+ return {
349
+ applied: true,
350
+ issueStatus: current.status,
351
+ nextWorkItemId: null,
352
+ humanActionId: null,
353
+ instanceCompleted: false,
354
+ triggerReflect: false,
355
+ };
356
+ }
357
+ // No head SHA to pin the verification to (no handoff recorded one): the
358
+ // gate cannot name a head, so escalate instead of merging — retry_merge
359
+ // re-enters this same finalize once a head exists and gates normally.
360
+ // Repeat finalizes reuse the one open escalation (no duplicate action or
361
+ // event), and the transition is skipped when already parked
362
+ // (needs_human has no self-loop).
363
+ if (!head) {
364
+ const retry = findOpenHumanActionByRequestId(issueId, "policy_escalation", OPERATOR_VERIFICATION_NO_HEAD_REQUEST_ID);
365
+ if (retry) {
366
+ if (current.status !== "needs_human") {
367
+ transitionIssue(issueId, "needs_human", {
368
+ currentOwner: "human",
369
+ currentIntent: OPERATOR_VERIFICATION_INTENT,
370
+ });
371
+ }
372
+ return {
373
+ applied: true,
374
+ issueStatus: "needs_human",
375
+ nextWorkItemId: null,
376
+ humanActionId: retry.id,
377
+ instanceCompleted: false,
378
+ triggerReflect: false,
379
+ };
380
+ }
381
+ const reason = `Reviewer approved, but ${criteria.length} acceptance ` +
382
+ `${criteria.length === 1 ? "criterion requires" : "criteria require"} a human operator ` +
383
+ `to verify (tagged [operator] in the frozen task snapshot) and no head SHA is ` +
384
+ `recorded to pin the verification to. The PR stays unmerged.`;
385
+ if (current.status !== "needs_human") {
386
+ transitionIssue(issueId, "needs_human", {
387
+ currentOwner: "human",
388
+ currentIntent: reason,
389
+ });
390
+ }
391
+ const action = createHumanAction({
392
+ issueId,
393
+ workflowInstanceId: active.id,
394
+ actionType: "policy_escalation",
395
+ reason,
396
+ question: `${reason} Retry the merge once a head is recorded, queue another repair round, or close the issue?`,
397
+ evidence: { [MERGE_FAILURE_EVIDENCE_KEY]: true, operatorVerificationBlocked: true },
398
+ responseOptions: [...MERGE_FAILURE_RESPONSE_OPTIONS],
399
+ requestId: OPERATOR_VERIFICATION_NO_HEAD_REQUEST_ID,
400
+ });
401
+ appendWorkflowEvent({
402
+ issueId,
403
+ workflowInstanceId: active.id,
404
+ workerSessionId: null,
405
+ type: "human_action.requested",
406
+ actorType: "system",
407
+ stage: "needs_human",
408
+ round: current.currentRound,
409
+ payload: { actionType: "policy_escalation", actionId: action.id, operatorVerificationBlocked: true },
410
+ });
411
+ return {
412
+ applied: true,
413
+ issueStatus: "needs_human",
414
+ nextWorkItemId: null,
415
+ humanActionId: action.id,
416
+ instanceCompleted: false,
417
+ triggerReflect: false,
418
+ };
419
+ }
420
+ const existing = findOpenHumanAction(issueId, "operator_verification");
421
+ if (existing && operatorActionHeadSha(existing) === head) {
422
+ if (current.status !== "needs_human") {
423
+ transitionIssue(issueId, "needs_human", {
424
+ currentOwner: "human",
425
+ currentIntent: OPERATOR_VERIFICATION_INTENT,
426
+ });
427
+ }
428
+ return {
429
+ applied: true,
430
+ issueStatus: "needs_human",
431
+ nextWorkItemId: null,
432
+ humanActionId: existing.id,
433
+ instanceCompleted: false,
434
+ triggerReflect: false,
435
+ };
436
+ }
437
+ const shortHead = head.slice(0, 8);
438
+ const reason = `Reviewer approved ${shortHead}, but ${criteria.length} acceptance ` +
439
+ `${criteria.length === 1 ? "criterion requires" : "criteria require"} a human operator ` +
440
+ `to verify (tagged [operator] in the frozen task snapshot) and no result is recorded ` +
441
+ `for this head. The PR stays unmerged until the result is recorded:\n` +
442
+ formatOperatorCriteria(criteria).join("\n");
443
+ transitionIssue(issueId, "needs_human", {
444
+ currentOwner: "human",
445
+ currentIntent: OPERATOR_VERIFICATION_INTENT,
446
+ });
447
+ // Head-pinned request_id: a concurrent finalize for the same head dedupes
448
+ // onto this action through createHumanAction's open-request conflict path;
449
+ // new commits after the gate get a fresh action with their own commands.
450
+ const action = createHumanAction({
451
+ issueId,
452
+ workflowInstanceId: active.id,
453
+ actionType: "operator_verification",
454
+ reason,
455
+ question: `${reason}\n\nPaste the probe output to record verification, waive with a reason, ` +
456
+ `or send the work back for another repair round?`,
457
+ evidence: { operatorVerification: { criteria, headSha: head } },
458
+ responseOptions: [...OPERATOR_VERIFICATION_RESPONSE_OPTIONS],
459
+ requestId: operatorVerificationRequestId(head),
460
+ });
461
+ appendWorkflowEvent({
462
+ issueId,
463
+ workflowInstanceId: active.id,
464
+ workerSessionId: null,
465
+ type: "human_action.requested",
466
+ actorType: "system",
467
+ stage: "needs_human",
468
+ round: current.currentRound,
469
+ payload: { actionType: "operator_verification", actionId: action.id, headSha: head },
470
+ });
471
+ return {
472
+ applied: true,
473
+ issueStatus: "needs_human",
474
+ nextWorkItemId: null,
475
+ humanActionId: action.id,
476
+ instanceCompleted: false,
477
+ triggerReflect: false,
478
+ };
479
+ })();
480
+ }
481
+ /** The head SHA an `operator_verification` action gates (null when unreadable). */
482
+ function operatorActionHeadSha(action) {
483
+ if (!action.evidenceJson)
484
+ return null;
485
+ try {
486
+ const evidence = JSON.parse(action.evidenceJson);
487
+ const headSha = evidence.operatorVerification?.headSha;
488
+ return typeof headSha === "string" && headSha ? headSha : null;
489
+ }
490
+ catch {
491
+ return null;
492
+ }
493
+ }
290
494
  function escalateMergeFailure(issue, workflowInstanceId, reason, evidence = { [MERGE_FAILURE_EVIDENCE_KEY]: true }) {
291
495
  return getDb().transaction(() => {
292
496
  const current = getIssue(issue.id);
@@ -0,0 +1,19 @@
1
+ // packages/server/src/coordinator/checks-wait-config.ts
2
+ //
3
+ // NOT-311: how long one work item may wait on CI before the wait itself escalates.
4
+ // The per-poll bound (CHECKS_POLL_TIMEOUT_MS, default 10 min) only ends a single
5
+ // poll with a `checks_pending` deferral; this ceiling bounds the whole wait measured
6
+ // from the first poll, across deferrals. Past it the item stops re-queueing and a
7
+ // human is asked to look at the stuck CI run instead of waiting forever.
8
+ /** Overall CI-wait ceiling measured from the first checks poll (default 2 h). */
9
+ export function checksWaitCeilingMs() {
10
+ const parsed = Number(process.env.CHECKS_WAIT_CEILING_MS ?? 2 * 60 * 60_000);
11
+ return Number.isFinite(parsed) ? parsed : 2 * 60 * 60_000;
12
+ }
13
+ /** True once `nowMs` is at or past the ceiling after the first wait started. */
14
+ export function checksWaitCeilingExceeded(firstWaitStartedAt, nowMs = Date.now()) {
15
+ const start = Date.parse(firstWaitStartedAt);
16
+ if (!Number.isFinite(start))
17
+ return false;
18
+ return nowMs - start >= checksWaitCeilingMs();
19
+ }