@yemi33/minions 0.1.2448 → 0.1.2449
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/bin/cli-api-client.js +1 -1
- package/bin/install-internal-minions.js +1382 -44
- package/bin/install-layout.js +150 -0
- package/bin/minions.js +460 -167
- package/dashboard/docs/typography.md +65 -12
- package/dashboard/js/command-center.js +58 -4
- package/dashboard/js/detail-panel.js +36 -0
- package/dashboard/js/memory-panel.js +59 -12
- package/dashboard/js/qa.js +179 -20
- package/dashboard/js/refresh.js +148 -12
- package/dashboard/js/render-dispatch.js +3 -4
- package/dashboard/js/render-inbox.js +2 -2
- package/dashboard/js/render-other.js +3 -3
- package/dashboard/js/render-pipelines.js +14 -0
- package/dashboard/js/render-plans.js +57 -9
- package/dashboard/js/render-prd.js +132 -23
- package/dashboard/js/render-prs.js +195 -166
- package/dashboard/js/render-schedules.js +63 -3
- package/dashboard/js/render-utils.js +3 -3
- package/dashboard/js/render-watches.js +19 -3
- package/dashboard/js/render-work-items.js +238 -30
- package/dashboard/js/settings.js +205 -54
- package/dashboard/js/utils.js +51 -1
- package/dashboard/pages/home.html +1 -1
- package/dashboard/pages/qa.html +1 -16
- package/dashboard/pages/work.html +40 -0
- package/dashboard/shared/cc-limits.js +79 -0
- package/dashboard/shared/pr-filters.js +21 -38
- package/dashboard/shared/project-git-summary.js +1 -1
- package/dashboard/shared/record-filters.js +169 -0
- package/dashboard/shared/watches-source.js +1 -1
- package/dashboard/shared/welcome-popup.js +1 -1
- package/dashboard/shared/wi-filters.js +302 -0
- package/dashboard/slim/body.html +1 -0
- package/dashboard/slim/js/command-send.js +26 -0
- package/dashboard/slim/js/modals-tiles.js +380 -39
- package/dashboard/slim/js/status.js +13 -21
- package/dashboard/slim/layout.html +1 -0
- package/dashboard/slim/panel-bootstrap.js +6 -2
- package/dashboard/slim/styles.css +38 -0
- package/dashboard/styles.css +159 -55
- package/dashboard-build.js +13 -2
- package/dashboard.js +956 -423
- package/docs/README.md +11 -6
- package/docs/api-errors.md +2 -2
- package/docs/architecture-review-2026-07-09.md +1 -1
- package/docs/architecture.excalidraw +2 -2
- package/docs/auto-discovery.md +18 -9
- package/docs/branch-derivation.md +4 -4
- package/docs/capture-demos.js +39 -2
- package/docs/ci-runner-canary.md +123 -0
- package/docs/claude-md-propagation.md +2 -2
- package/docs/cloud-agent-dispatch.md +204 -0
- package/docs/command-center.md +7 -7
- package/docs/completion-reports.md +43 -20
- package/docs/constants.md +10 -3
- package/docs/constellation-bridge.md +134 -6
- package/docs/constellation-style-telemetry.md +4 -4
- package/docs/contracts/capability-protocol.v1.json +165 -0
- package/docs/cooldown-merge-semantics.md +12 -12
- package/docs/copilot-cli-schema.md +7 -7
- package/docs/cross-repo-plans.md +10 -10
- package/docs/dead-code-audit-retractions.md +5 -5
- package/docs/default-branch-ci.md +173 -0
- package/docs/deprecated.json +31 -31
- package/docs/design-inbox-entries-schema.md +3 -3
- package/docs/design-language.md +1051 -0
- package/docs/design-state-storage.md +11 -11
- package/docs/diagnostics-crash-reports.md +9 -9
- package/docs/diagnostics-memory.md +5 -5
- package/docs/documentation-audit-2026-07-09.md +7 -7
- package/docs/engine-restart.md +90 -5
- package/docs/harness-mode.md +1 -1
- package/docs/internal-install.md +338 -39
- package/docs/kb-dedup-duplicate-pair-investigation.md +5 -5
- package/docs/kb-pr3223-cascade-archiving.md +1 -1
- package/docs/kb-pr696-merge-conflict-docs.md +6 -6
- package/docs/kb-sweep.md +35 -35
- package/docs/keep-processes.md +1 -1
- package/docs/live-checkout-mode.md +30 -30
- package/docs/managed-spawn.md +18 -14
- package/docs/named-agents.md +7 -7
- package/docs/plan-lifecycle.md +69 -2
- package/docs/pr-author-identity.md +114 -0
- package/docs/pr-auto-fix-dispatch.md +19 -4
- package/docs/pr-comment-followup.md +6 -6
- package/docs/pr-review-fix-loop.md +59 -10
- package/docs/process-termination.md +40 -0
- package/docs/proposals/repo-pool-for-live-checkout.md +13 -13
- package/docs/qa-runbook-lifecycle.md +367 -17
- package/docs/qa-runbooks.md +3 -3
- package/docs/rfc-completion-json.md +18 -18
- package/docs/runtime-adapters.md +26 -21
- package/docs/security.md +6 -6
- package/docs/self-improvement.md +4 -4
- package/docs/shared-lifecycle-module-map.md +473 -472
- package/docs/skills.md +52 -3
- package/docs/slim-ux/concepts.md +121 -116
- package/docs/specs/agent-configurability.md +18 -18
- package/docs/specs/agent-rename.md +18 -18
- package/docs/team-memory.md +38 -21
- package/docs/timeouts-and-liveness.md +118 -10
- package/docs/tutorials/01-install-and-connect.md +1 -1
- package/docs/watches.md +40 -39
- package/docs/workspace-manifests.md +4 -4
- package/docs/worktree-lifecycle.md +293 -14
- package/engine/README.md +46 -0
- package/engine/{ado-comment.js → ado/comment.js} +8 -8
- package/engine/{ado-git-auth.js → ado/git-auth.js} +4 -4
- package/engine/{ado.js → ado/index.js} +417 -63
- package/engine/{ado-status.js → ado/status.js} +6 -8
- package/engine/{ado-token.js → ado/token.js} +1 -1
- package/engine/{acp-transport.js → agents/acp-transport.js} +62 -22
- package/engine/{agent-worker-pool.js → agents/agent-worker-pool.js} +17 -8
- package/engine/{cc-worker-pool.js → agents/cc-worker-pool.js} +16 -6
- package/engine/{claude-md-context.js → agents/claude-md-context.js} +5 -5
- package/engine/{harness-context.js → agents/harness-context.js} +5 -5
- package/engine/{harness.js → agents/harness.js} +3 -3
- package/engine/{llm.js → agents/llm.js} +18 -14
- package/engine/{model-discovery.js → agents/model-discovery.js} +2 -2
- package/engine/{playbook.js → agents/playbook.js} +155 -22
- package/engine/{pooled-agent-process.js → agents/pooled-agent-process.js} +14 -12
- package/engine/{preflight.js → agents/preflight.js} +29 -10
- package/engine/{spawn-agent.js → agents/spawn-agent.js} +25 -14
- package/engine/{spawn-phase-watchdog.js → agents/spawn-phase-watchdog.js} +16 -7
- package/engine/{steering.js → agents/steering.js} +5 -5
- package/engine/{tools-inventory.js → agents/tools-inventory.js} +2 -2
- package/engine/{agent-api-validation.js → api/agent-api-validation.js} +2 -2
- package/engine/{api-validation.js → api/api-validation.js} +1 -1
- package/engine/api/bridge.js +787 -0
- package/engine/{cc-api-validation.js → api/cc-api-validation.js} +1 -1
- package/engine/api/companion.js +560 -0
- package/engine/{content-api-validation.js → api/content-api-validation.js} +2 -2
- package/engine/{pr-issue-validation.js → api/pr-issue-validation.js} +33 -6
- package/engine/{settings-validation.js → api/settings-validation.js} +32 -4
- package/engine/api-contracts/agent-content.js +4 -4
- package/engine/api-contracts/capability-manifest.js +236 -0
- package/engine/api-contracts/capability-protocol.js +333 -0
- package/engine/api-contracts/cc-ops.js +1 -1
- package/engine/api-contracts/config-runtime.js +5 -0
- package/engine/api-contracts/core.js +28 -1
- package/engine/api-contracts/index.js +100 -0
- package/engine/api-contracts/orchestration.js +18 -5
- package/engine/api-contracts/pull-requests.js +37 -6
- package/engine/api-contracts/qa-process.js +29 -6
- package/engine/api-contracts/work-plan-prd.js +21 -1
- package/engine/cloud/contract.js +212 -0
- package/engine/cloud/index.js +159 -0
- package/engine/{execution-model.js → core/execution-model.js} +1 -1
- package/engine/{features.js → core/features.js} +4 -4
- package/engine/{operator-identity.js → core/operator-identity.js} +1 -1
- package/engine/{queries.js → core/queries.js} +201 -36
- package/engine/{safe-expr.js → core/safe-expr.js} +1 -1
- package/engine/{shared.js → core/shared.js} +1637 -175
- package/engine/{stdio-timestamps.js → core/stdio-timestamps.js} +1 -1
- package/engine/{untrusted-fence.js → core/untrusted-fence.js} +3 -3
- package/engine/db/index.js +11 -2
- package/engine/db/migrations/002-dispatches.js +3 -3
- package/engine/db/migrations/003-work-items.js +1 -1
- package/engine/db/migrations/004-pull-requests.js +1 -1
- package/engine/db/migrations/006-metrics.js +1 -1
- package/engine/db/migrations/007-watches.js +2 -2
- package/engine/db/migrations/008-small-state.js +1 -1
- package/engine/db/migrations/009-qa.js +1 -1
- package/engine/db/migrations/010-pr-links.js +1 -1
- package/engine/db/migrations/011-remaining-state.js +1 -1
- package/engine/db/migrations/012-steering-deliveries.js +2 -2
- package/engine/db/migrations/013-backfill-broken-note-links.js +1 -1
- package/engine/db/migrations/014-pr-fix-target-prefs.js +2 -2
- package/engine/db/migrations/015-plans-prds.js +0 -0
- package/engine/db/migrations/018-sql-only-cutover.js +2 -2
- package/engine/db/migrations/021-archived-work-items.js +1 -1
- package/engine/db/migrations/022-global-cc-session.js +1 -1
- package/engine/db/migrations/023-engine-state.js +1 -1
- package/engine/db/migrations/025-malformed-work-item-phantoms.js +1 -1
- package/engine/db/migrations/027-review-learning-lifecycle.js +1 -1
- package/engine/db/migrations/029-repair-reused-versions.js +20 -0
- package/engine/db/migrations/031-pr-author-identity.js +137 -0
- package/engine/{consolidation.js → memory/consolidation.js} +6 -6
- package/engine/{kb-sweep-runner.js → memory/kb-sweep-runner.js} +2 -2
- package/engine/{kb-sweep.js → memory/kb-sweep.js} +9 -7
- package/engine/{memory-retrieval.js → memory/memory-retrieval.js} +46 -4
- package/engine/{memory-store.js → memory/memory-store.js} +3 -3
- package/engine/{promotion.js → memory/promotion.js} +3 -3
- package/engine/{review-learning-backfill.js → memory/review-learning-backfill.js} +6 -6
- package/engine/{review-learning.js → memory/review-learning.js} +10 -5
- package/engine/{diagnostics-memory.js → observability/diagnostics-memory.js} +1 -1
- package/engine/{logs-store.js → observability/logs-store.js} +5 -5
- package/engine/{metrics-store.js → observability/metrics-store.js} +4 -4
- package/engine/{check-status.js → operations/check-status.js} +3 -3
- package/engine/{cli.js → operations/cli.js} +271 -113
- package/engine/{distribution.js → operations/distribution.js} +5 -6
- package/engine/{cleanup.js → orchestration/cleanup.js} +72 -45
- package/engine/{cooldown.js → orchestration/cooldown.js} +5 -5
- package/engine/{dispatch-events.js → orchestration/dispatch-events.js} +2 -2
- package/engine/{dispatch.js → orchestration/dispatch.js} +129 -36
- package/engine/orchestration/failed-scheduled-cleanup.js +274 -0
- package/engine/{lifecycle.js → orchestration/lifecycle.js} +198 -90
- package/engine/{meeting.js → orchestration/meeting.js} +6 -16
- package/engine/{pipeline.js → orchestration/pipeline.js} +12 -12
- package/engine/{pre-dispatch-eval.js → orchestration/pre-dispatch-eval.js} +10 -9
- package/engine/{routing.js → orchestration/routing.js} +3 -3
- package/engine/{schedule-bootstrap.js → orchestration/schedule-bootstrap.js} +4 -4
- package/engine/{scheduler.js → orchestration/scheduler.js} +38 -8
- package/engine/{timeout.js → orchestration/timeout.js} +158 -109
- package/engine/{db-events.js → persistence/db-events.js} +2 -2
- package/engine/{dispatch-store.js → persistence/dispatch-store.js} +7 -7
- package/engine/{inbox-store.js → persistence/inbox-store.js} +2 -2
- package/engine/{note-link-backfill.js → persistence/note-link-backfill.js} +4 -4
- package/engine/{pr-fix-target-store.js → persistence/pr-fix-target-store.js} +8 -8
- package/engine/{pull-requests-store.js → persistence/pull-requests-store.js} +21 -7
- package/engine/{small-state-store.js → persistence/small-state-store.js} +31 -31
- package/engine/persistence/state-operations.js +350 -0
- package/engine/{steering-store.js → persistence/steering-store.js} +6 -6
- package/engine/{issues.js → planning/issues.js} +2 -2
- package/engine/{plan-prd-validation.js → planning/plan-prd-validation.js} +8 -2
- package/engine/planning/prd-result-sidecar.js +190 -0
- package/engine/{prd-store.js → planning/prd-store.js} +17 -17
- package/engine/{project-discovery.js → planning/project-discovery.js} +5 -5
- package/engine/{projects.js → planning/projects.js} +10 -10
- package/engine/{resolve-area.js → planning/resolve-area.js} +1 -1
- package/engine/{work-item-validation.js → planning/work-item-validation.js} +39 -3
- package/engine/{work-items-store.js → planning/work-items-store.js} +29 -21
- package/engine/{keep-process-sweep.js → processes/keep-process-sweep.js} +57 -17
- package/engine/{managed-spawn-launcher.js → processes/managed-spawn-launcher.js} +3 -3
- package/engine/{managed-spawn.js → processes/managed-spawn.js} +97 -46
- package/engine/{process-utils.js → processes/process-utils.js} +599 -55
- package/engine/{abandoned-pr-reconciliation.js → providers/abandoned-pr-reconciliation.js} +17 -7
- package/engine/{comment-classifier.js → providers/comment-classifier.js} +85 -17
- package/engine/{comment-format.js → providers/comment-format.js} +5 -5
- package/engine/{gh-comment.js → providers/gh-comment.js} +15 -15
- package/engine/{gh-token.js → providers/gh-token.js} +4 -4
- package/engine/{github.js → providers/github.js} +131 -54
- package/engine/{pr-action.js → providers/pr-action.js} +13 -12
- package/engine/{pr-clone-keep.js → providers/pr-clone-keep.js} +7 -7
- package/engine/{pr-devbox.js → providers/pr-devbox.js} +6 -6
- package/engine/{pr-fix-target.js → providers/pr-fix-target.js} +13 -13
- package/engine/{pr-remote-patch.js → providers/pr-remote-patch.js} +4 -4
- package/engine/{pr-resolve.js → providers/pr-resolve.js} +7 -7
- package/engine/{pr-temp-clone.js → providers/pr-temp-clone.js} +5 -5
- package/engine/{pr-track.js → providers/pr-track.js} +11 -13
- package/engine/{shared-branch-pr-reconcile.js → providers/shared-branch-pr-reconcile.js} +4 -4
- package/engine/qa/auto-prd-qa.js +313 -0
- package/engine/{qa-from-prd.js → qa/from-prd.js} +42 -12
- package/engine/qa/prd-session.js +240 -0
- package/engine/{qa-process-validation.js → qa/process-validation.js} +14 -9
- package/engine/{qa-runbooks.js → qa/runbooks.js} +1 -1
- package/engine/{qa-runs.js → qa/runs.js} +286 -15
- package/engine/{qa-sessions.js → qa/sessions.js} +595 -49
- package/engine/qa/visual-journey.js +654 -0
- package/engine/{qa-runners.js → qa-runners/index.js} +7 -7
- package/engine/qa-runners/maestro.js +3 -3
- package/engine/qa-runners/playwright.js +2 -2
- package/engine/{restart-health.js → recovery/restart-health.js} +48 -4
- package/engine/recovery/stop-stack.js +607 -0
- package/engine/{supervisor.js → recovery/supervisor.js} +105 -175
- package/engine/{watchdog.js → recovery/watchdog.js} +136 -13
- package/engine/runtimes/claude.js +14 -12
- package/engine/runtimes/codex.js +8 -6
- package/engine/runtimes/copilot.js +17 -16
- package/engine/{watch-actions.js → watches/actions.js} +13 -13
- package/engine/{watches.js → watches/index.js} +43 -32
- package/engine/{watches-store.js → watches/store.js} +4 -4
- package/engine/{create-pr-worktree.js → worktrees/create-pr.js} +1 -1
- package/engine/{worktree-gc.js → worktrees/gc.js} +70 -22
- package/engine/worktrees/inventory.js +671 -0
- package/engine/{live-checkout.js → worktrees/live-checkout.js} +4 -4
- package/engine/{worktree-pool.js → worktrees/pool.js} +2 -2
- package/engine/{worktree-preflight.js → worktrees/preflight.js} +1 -0
- package/engine/worktrees/quarantine-refs.js +173 -0
- package/engine.js +1137 -208
- package/minions.js +147 -77
- package/package.json +10 -6
- package/playbooks/_pr-description-audit.md +110 -78
- package/playbooks/build-fix-complex.md +2 -0
- package/playbooks/fix.md +16 -12
- package/playbooks/implement-shared.md +2 -0
- package/playbooks/implement.md +19 -20
- package/playbooks/plan-to-prd.md +18 -3
- package/playbooks/qa-session-draft.md +136 -1
- package/playbooks/qa-session-execute.md +80 -2
- package/playbooks/qa-session-setup.md +17 -1
- package/playbooks/qa-validate.md +1 -1
- package/playbooks/setup.md +2 -0
- package/playbooks/shared-rules.md +25 -32
- package/playbooks/templates/followup-dispatch.md +4 -3
- package/playbooks/verify.md +1 -1
- package/prompts/cc-system.md +19 -27
- package/watch-plugins/README.md +92 -0
- package/watch-plugins/ado-author-prs.js +336 -0
- package/watch-plugins/gh-author-prs.js +375 -0
- package/watch-plugins/http.js +474 -0
- package/watch-plugins/teams-channel.js +869 -0
- package/docs/dev-composite-workflow.md +0 -101
- package/docs/pr-screenshots/pr-886/after-single-header.png +0 -0
- package/docs/pr-screenshots/pr-886/before-duplicate-header.png +0 -0
- package/docs/pr-screenshots/pr-895/01-cancellation-reason-detail.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-901/projects-tab-default.png +0 -0
- package/docs/pr-screenshots/pr-901/projects-tab-fmf-selected.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-AFTER-crop.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-BEFORE-crop.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-dropdown-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
- package/docs/visual-evidence-ci.md +0 -103
- package/engine/bridge.js +0 -379
- package/engine/quarantine-refs.js +0 -103
- package/engine/state-operations.js +0 -178
- /package/engine/{steering-constraints.js → agents/steering-constraints.js} +0 -0
|
@@ -47,6 +47,34 @@ dashboard page renders a per-row Delete button conditionally (terminal
|
|
|
47
47
|
only), confirms with the user, and surfaces success/error via
|
|
48
48
|
`showToast()` rather than `alert()`.
|
|
49
49
|
|
|
50
|
+
Success replies carry `deletedAs`: `terminal` for the normal path,
|
|
51
|
+
`orphaned-pending` for the escape hatch below.
|
|
52
|
+
|
|
53
|
+
#### Orphaned pending runs (`?orphaned=1`)
|
|
54
|
+
|
|
55
|
+
Terminal-only deletion leaves one record class permanently stuck: a
|
|
56
|
+
`pending` run written by a stray process that was never dispatched and owns
|
|
57
|
+
nothing. `DELETE /api/qa/runs/<id>?orphaned=1` opts into removing it, but
|
|
58
|
+
only when `engine/qa/runs.js#evaluateOrphanedPendingRun` can prove *every*
|
|
59
|
+
one of these:
|
|
60
|
+
|
|
61
|
+
- status is `pending` (a `running` run is never eligible);
|
|
62
|
+
- `startedAt` and `completedAt` are unset and `artifacts` is empty;
|
|
63
|
+
- `workItemId` is unset and no non-archived work item carries
|
|
64
|
+
`meta.qaRunId === <id>`;
|
|
65
|
+
- no QA session carries `qaRunId === <id>`;
|
|
66
|
+
- the record is at least `QA_ORPHAN_MIN_AGE_MS` old (24h), which covers the
|
|
67
|
+
window where a live dispatch is still back-filling its linkage;
|
|
68
|
+
- `getRunbook(run.runbookId)` returns null **and**
|
|
69
|
+
`getManagedSpecByName(run.targetName)` returns null.
|
|
70
|
+
|
|
71
|
+
Any unreadable ownership source fails closed (treated as *not* orphaned).
|
|
72
|
+
A pending run that misses any condition returns
|
|
73
|
+
`409 { error: 'not_orphaned', currentStatus, reason }`, where `reason` names
|
|
74
|
+
the first failed condition (`too_recent`, `runbook_exists`, `session_owned`,
|
|
75
|
+
…). `?orphaned=1` is additive only — it never relaxes the refusal for
|
|
76
|
+
`running` runs or for ambiguously-owned pending runs.
|
|
77
|
+
|
|
50
78
|
## Artifact contract
|
|
51
79
|
|
|
52
80
|
Artifacts live at `<MINIONS_DIR>/engine/qa-artifacts/<runId>/<path>`, served via
|
|
@@ -105,7 +133,7 @@ runner-native test file, and (with user approval) a second agent executes
|
|
|
105
133
|
it. Sessions are a thin orchestration layer on top of the same
|
|
106
134
|
`managed-spawn` + `qa-run-result.json` infrastructure that powers
|
|
107
135
|
runbooks above — they reuse the SQL QA-run store, `engine/qa-artifacts/`, and
|
|
108
|
-
the existing `engine/lifecycle.js#runPostCompletionHooks` qa-run sidecar
|
|
136
|
+
the existing `engine/orchestration/lifecycle.js#runPostCompletionHooks` qa-run sidecar
|
|
109
137
|
hook. Surfaced on `/qa` (sessions card list above the runbooks/runs
|
|
110
138
|
tables) and proxied by the Command Center natural-language shortcut.
|
|
111
139
|
|
|
@@ -127,7 +155,7 @@ runbooks for repeat traffic.
|
|
|
127
155
|
|
|
128
156
|
## State machine
|
|
129
157
|
|
|
130
|
-
Eight states, source of truth `engine/qa
|
|
158
|
+
Eight states, source of truth `engine/qa/sessions.js#QA_SESSION_STATE`:
|
|
131
159
|
|
|
132
160
|
```
|
|
133
161
|
pending ──▶ spawning ──▶ drafting ──▶ awaiting-approval ──▶ executing ──▶ done
|
|
@@ -168,9 +196,9 @@ routing default):
|
|
|
168
196
|
**Multi-project fan-out (W-mpq6xqzj000606d0):** when `spec.projects`
|
|
169
197
|
contains more than one project name, SETUP is fanned out — one work item
|
|
170
198
|
per project, queued in parallel into each project's own
|
|
171
|
-
the target project's SQL work-item scope.
|
|
172
|
-
|
|
173
|
-
|
|
199
|
+
the target project's SQL work-item scope. One project is the **primary**
|
|
200
|
+
(`meta.qaSession.primary === true`); the rest are **co-services**
|
|
201
|
+
(`primary === false`). Each WI's
|
|
174
202
|
`meta.qaSession.coServices` lists the co-service project names (only the
|
|
175
203
|
primary carries the full list; co-services see `[]`) and
|
|
176
204
|
`meta.qaSession.primaryProject` carries the canonical primary name.
|
|
@@ -185,6 +213,35 @@ routing default):
|
|
|
185
213
|
Single-project sessions skip `setupStatus` entirely (fast path uses
|
|
186
214
|
`session.state` directly).
|
|
187
215
|
|
|
216
|
+
**Explicit primary + per-project targets (W-msb9kgs402813a97-a).** Which
|
|
217
|
+
project is primary and which target each project is checked out at are
|
|
218
|
+
**declared**, not positional:
|
|
219
|
+
|
|
220
|
+
- `spec.primaryProject: string` — must be a member of the resolved
|
|
221
|
+
`projects[]` (`validateSpec` rejects anything else with an error naming
|
|
222
|
+
the value and the known projects). Defaults to `projects[0]`.
|
|
223
|
+
`session.coServices` is then *every other* project, so a primary in the
|
|
224
|
+
middle of the list still fans the rest out correctly.
|
|
225
|
+
- `spec.projectTargets: { [projectName]: target }` — per-project target
|
|
226
|
+
overrides. Each key must be a member of `projects[]`, and each value is
|
|
227
|
+
validated by the same `_validateTarget` rules as `spec.target`. Each
|
|
228
|
+
SETUP WI is built with `spec.projectTargets[project] || spec.target`
|
|
229
|
+
(`meta.qaSession.target`, the prompt body, and the WI title all reflect
|
|
230
|
+
the per-project target), so a co-service is no longer forced onto the
|
|
231
|
+
primary's branch/PR/commit. DRAFT and EXECUTE inherit the primary's
|
|
232
|
+
target.
|
|
233
|
+
|
|
234
|
+
Both fields ride the normal request allowlist
|
|
235
|
+
(`engine/qa/process-validation.js#validateQaSessionRequest`), are ordered
|
|
236
|
+
and marked by the shared `orderTargetsByPrimary` seam in both project
|
|
237
|
+
resolvers (`dashboard.js#_qaSessionsResolveTargets` and
|
|
238
|
+
`engine/qa/prd-session.js#resolveTargetsFromConfig`), and are rewritten to
|
|
239
|
+
canonical project names by `canonicalizeResolvedProjects`. `queueSetup`
|
|
240
|
+
reorders the resolved targets so the declared primary leads, and throws
|
|
241
|
+
before `markSpawning` when project-bound targets cannot host it — a
|
|
242
|
+
contract disagreement surfaces as a 4xx instead of a session stranded in
|
|
243
|
+
`spawning`.
|
|
244
|
+
|
|
188
245
|
2. **DRAFT** (`playbooks/qa-session-draft.md`) reads the live spawn
|
|
189
246
|
metadata via `/api/managed-processes/by-name/qa-session-<id>`, calls the
|
|
190
247
|
resolved runner's `generateBrief({target, flowsRaw, capture})` hook, and
|
|
@@ -192,7 +249,56 @@ routing default):
|
|
|
192
249
|
the injected absolute `engine/qa-tests/<sessionId>/test.<ext>` path.
|
|
193
250
|
The completion report's `testFile` value is stored on the session. In
|
|
194
251
|
`confirm` mode the session parks at `awaiting-approval`; in `auto` mode
|
|
195
|
-
it auto-chains to EXECUTE
|
|
252
|
+
it auto-chains to EXECUTE — but only after the visual-journey gate below
|
|
253
|
+
accepts the draft.
|
|
254
|
+
|
|
255
|
+
### Visual-journey manifest + pre-EXECUTE gate (W-msb9kgs402813a97-b)
|
|
256
|
+
|
|
257
|
+
`structuredCompletion.testFile` used to be DRAFT's only structured output, so
|
|
258
|
+
an API-only test file passed straight through `handleDraftComplete` into
|
|
259
|
+
EXECUTE: a session that asked for screenshots or video could reach a green
|
|
260
|
+
`qa-run` having never opened a browser, and a multi-project session could
|
|
261
|
+
ignore every co-service origin.
|
|
262
|
+
|
|
263
|
+
`engine/qa/visual-journey.js` owns the fix. It is a **declaration** validated by
|
|
264
|
+
the session layer, never a source-string scan of the drafted test file (a guess
|
|
265
|
+
cannot be a gate).
|
|
266
|
+
|
|
267
|
+
- **Sidecar.** DRAFT writes `agents/<agentId>/qa-session-draft-result.json`
|
|
268
|
+
(injected as the `{{qa_draft_result_sidecar}}` template var). The contract
|
|
269
|
+
mirrors `engine/qa/runs.js#prepareResultSidecar` / `#consumeResultSidecar`:
|
|
270
|
+
`engine.js#spawnAgent` clears stale content before a DRAFT dispatch (load-
|
|
271
|
+
bearing on the `editDraft` re-draft path, which reuses the session id), and
|
|
272
|
+
`engine/orchestration/lifecycle.js` does one atomic read-then-unlink on every
|
|
273
|
+
DRAFT completion — success or failure — so a manifest can never be inherited
|
|
274
|
+
by the next attempt. Missing / unreadable / malformed / session-id-mismatched
|
|
275
|
+
files return `{ ok: false, reason }`.
|
|
276
|
+
- **Shape.** `{ sessionId, journeys: [{ id, name, kind: 'browser'|'api',
|
|
277
|
+
projects[], services[], origins[], steps[], assertions[],
|
|
278
|
+
evidence: { screenshots[], video[] } }] }`. Journey count, per-list length,
|
|
279
|
+
per-entry byte size, and the encoded manifest itself are all capped
|
|
280
|
+
(`visual-journey.js` `LIMITS`), so a runaway agent cannot inflate the session
|
|
281
|
+
record.
|
|
282
|
+
- **Gate.** Enforced by `validateVisualJourneyManifest(session, manifest,
|
|
283
|
+
{ services })` **only** when `session.spec.capture.video ||
|
|
284
|
+
session.spec.capture.screenshots` — a logs-only (or capture-less) session
|
|
285
|
+
keeps its historical pass-through behaviour. It rejects when: there is no
|
|
286
|
+
manifest at all; no journey has `kind: 'browser'` (an API-only draft cannot
|
|
287
|
+
produce visual evidence); some project in `session.spec.projects` appears in
|
|
288
|
+
no journey's `projects`; some name from
|
|
289
|
+
`managedSpawnNamesForSession(session)` appears in no journey's `services`; or
|
|
290
|
+
a requested capture type has no planned artifact of its extension
|
|
291
|
+
(`screenshots` ⇒ `.png`, `video` ⇒ `.webm`). Each rejection is a distinct,
|
|
292
|
+
actionable message naming the missing projects / services / evidence type.
|
|
293
|
+
- **Placement.** The gate runs inside `handleDraftComplete` **before**
|
|
294
|
+
`markAwaitingApproval` and **before** the auto-mode EXECUTE queue, so a
|
|
295
|
+
rejected draft is neither parked for human approval nor chained into
|
|
296
|
+
EXECUTE. A rejection calls `markFailed` with
|
|
297
|
+
`failure_class: 'qa-session-draft-visual-coverage'` and the joined error; the
|
|
298
|
+
auto-path `qa-run` created moments earlier is terminalized as `errored`
|
|
299
|
+
rather than left pending. An accepted manifest is persisted as
|
|
300
|
+
`session.visualJourneyManifest` for the EXECUTE-side evidence check to
|
|
301
|
+
validate against.
|
|
196
302
|
|
|
197
303
|
3. **EXECUTE** (`playbooks/qa-session-execute.md`) runs the drafted test
|
|
198
304
|
against the live spawn via the runner's `executeBrief` hook, captures
|
|
@@ -200,34 +306,141 @@ routing default):
|
|
|
200
306
|
the injected absolute `qa_result_sidecar` path — the same single-use sidecar
|
|
201
307
|
the `qa-validate` runbook flow uses. Artifacts go to the injected absolute
|
|
202
308
|
`qa_artifacts_dir` keyed by the linked QA run id, not the session id. The
|
|
203
|
-
`engine/lifecycle.js#runPostCompletionHooks` `meta.qaRunId` hook ingests
|
|
309
|
+
`engine/orchestration/lifecycle.js#runPostCompletionHooks` `meta.qaRunId` hook ingests
|
|
204
310
|
the sidecar and marks the linked `qa-runs` record terminal; the
|
|
205
311
|
session-level `handleExecuteComplete` then reads the `qa-run` terminal
|
|
206
312
|
status and transitions `executing → done` (or `failed`).
|
|
207
313
|
|
|
314
|
+
### EXECUTE evidence coverage (W-msb9kgs402813a97-c)
|
|
315
|
+
|
|
316
|
+
A green `qa-run` is necessary but **not sufficient** for a session that asked
|
|
317
|
+
for screenshots or video. Before this gate, `handleExecuteComplete` went to
|
|
318
|
+
`done` on `qaRunStatus === 'passed'` with no evidence check at all, and
|
|
319
|
+
`normalizeArtifact` accepted any `{type, path}` without touching the
|
|
320
|
+
filesystem — so a run whose only green assertions were API calls closed a
|
|
321
|
+
visual session, and a multi-project session could pass on evidence from a
|
|
322
|
+
single project.
|
|
323
|
+
|
|
324
|
+
- **Sidecar extension.** The `qa-run-result.json` contract gains
|
|
325
|
+
`journeyCoverage: [{ journeyId, status: 'passed'|'failed'|'skipped',
|
|
326
|
+
projects[], services[] }]` plus optional per-artifact `journeyId` and
|
|
327
|
+
`project`. `engine/qa/runs.js#normalizeJourneyCoverage` drops malformed
|
|
328
|
+
entries (unknown status, missing id, duplicates) rather than repairing them,
|
|
329
|
+
and `completeRun` stamps `run.journeyCoverage` **only** when the caller
|
|
330
|
+
supplied it, so legacy `qa-validate` runs keep their historical record shape.
|
|
331
|
+
- **Validator.** `engine/qa/visual-journey.js#validateEvidenceCoverage({
|
|
332
|
+
session, manifest, run, artifactsDir })` returns
|
|
333
|
+
`{ ok, required, missing: { files, journeys, projects, services,
|
|
334
|
+
evidenceTypes }, errors, error }`. For each requested capture type it asserts
|
|
335
|
+
(a) at least one artifact of that type is registered on the run,
|
|
336
|
+
(b) every such artifact resolves **inside** `qaArtifactsDirForRun(runId)`
|
|
337
|
+
(`shared.isPathInside`) and exists on disk as a non-zero-size regular file,
|
|
338
|
+
and (c) the union of `journeyCoverage[].projects` over **passed** journeys
|
|
339
|
+
covers every `session.spec.projects`, the union of `.services` covers every
|
|
340
|
+
`managedSpawnNamesForSession(session)` name, and every journey the DRAFT
|
|
341
|
+
manifest declared actually reported `passed`.
|
|
342
|
+
- **Manifest delivery.** Check (c) grades the run against journey ids the DRAFT
|
|
343
|
+
agent invented, so the EXECUTE prompt has to carry them: the DRAFT sidecar is
|
|
344
|
+
read-then-unlinked and the accepted manifest survives only on the session
|
|
345
|
+
record. `engine.js#buildQaExecuteJourneyContract(session)` projects
|
|
346
|
+
`session.visualJourneyManifest` into the sidecar's own coverage-entry shape
|
|
347
|
+
(`[{journeyId, name, kind, projects, services}]`) and
|
|
348
|
+
`_qaVisualJourneyManifestJson(item)` renders it as the
|
|
349
|
+
`{{visual_journey_manifest_json}}` template var consumed by
|
|
350
|
+
`playbooks/qa-session-execute.md`. It is EXECUTE-only (the manifest does not
|
|
351
|
+
exist before DRAFT produces it, and re-draft must not be shown the previous
|
|
352
|
+
attempt's ids), bounded independently of the DRAFT-side manifest caps, and
|
|
353
|
+
degrades to `''` for logs-only, legacy, and non-QA dispatches. Without it a
|
|
354
|
+
flawless visual run would always fail `qa-session-evidence-incomplete`.
|
|
355
|
+
- **Gate.** Run inside `handleExecuteComplete` **before** DONE is chosen. A
|
|
356
|
+
coverage failure **overrides** a green `qaRunStatus`: the session transitions
|
|
357
|
+
to `failed` with `failure_class: 'qa-session-evidence-incomplete'` and an
|
|
358
|
+
`error` naming exactly what was missing. An already-failing run keeps its own
|
|
359
|
+
`qa-session-execute-failed` / `-errored` class — the gate only ever downgrades
|
|
360
|
+
a would-be `done`. It is skipped entirely when both capture flags are false
|
|
361
|
+
or no `visualJourneyManifest` is persisted (a legacy session), so non-visual
|
|
362
|
+
and pre-manifest completions are untouched. An internal error inside the gate
|
|
363
|
+
fails **closed**.
|
|
364
|
+
|
|
208
365
|
Each phase WI carries `meta.sessionId`, `meta.sessionPhase`, `meta.qaSession`
|
|
209
366
|
(target + flowsRaw + mode + capture + runner), and `meta.playbook`. The
|
|
210
367
|
EXECUTE WI additionally carries `meta.qaRunId` so the existing qa-run
|
|
211
368
|
lifecycle hook fires.
|
|
212
369
|
|
|
370
|
+
### Co-service spawn metadata (W-msb9kgs402813a97-a)
|
|
371
|
+
|
|
372
|
+
A multi-project session owns one managed-spawn per project, but DRAFT/EXECUTE
|
|
373
|
+
historically only saw the PRIMARY one — a co-service origin was unaddressable
|
|
374
|
+
from the drafted test. `engine.js#buildQaSessionServices` now iterates
|
|
375
|
+
`qaSessions.managedSpawnNamesForSession(session)` and builds a bounded array
|
|
376
|
+
(cap 8) of
|
|
377
|
+
|
|
378
|
+
```json
|
|
379
|
+
[{ "name": "qa-session-<id>-api", "project": "api", "primary": false,
|
|
380
|
+
"health": "healthy", "baseUrl": "http://localhost:4000", "ports": [4000] }]
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
`health` is `healthy` | `unhealthy` (alive, failing its healthcheck) | `down`
|
|
384
|
+
(registered, not alive) | `missing` (no spec registered yet — the normal SETUP
|
|
385
|
+
state). It is surfaced two ways:
|
|
386
|
+
|
|
387
|
+
- as the `{{session_services_json}}` template var (registered in
|
|
388
|
+
`PLAYBOOK_OPTIONAL_VARS`, rendered by `playbooks/qa-session-draft.md` and
|
|
389
|
+
`playbooks/qa-session-execute.md` with prose stating each entry is a **real,
|
|
390
|
+
separately-reachable origin**, not a route on the primary), and
|
|
391
|
+
- as `briefOpts.services` on the runner adapter `generateBrief()` /
|
|
392
|
+
`executeBrief()` calls, so an adapter can address a co-service directly.
|
|
393
|
+
|
|
394
|
+
`managed_spawn_name` is unchanged and still names the **primary** spawn.
|
|
395
|
+
Discovery never fails a render: an unreadable managed-process store degrades to
|
|
396
|
+
`missing` rows and an empty string.
|
|
397
|
+
|
|
398
|
+
### Who dispatches a phase (W-ms9nq2pu00cd60f3)
|
|
399
|
+
|
|
400
|
+
`engine/qa/sessions.js` **persists** each phase work item and requests an
|
|
401
|
+
engine wakeup (`control.json._wakeupAt`); it never enqueues a dispatch record
|
|
402
|
+
of its own. The engine's normal work-discovery pass
|
|
403
|
+
(`engine.js#discoverFromWorkItems` for project scopes,
|
|
404
|
+
`#discoverCentralWorkItems` for central) builds the dispatch, and it is the
|
|
405
|
+
only place that stamps `meta.project`, sets `meta.source` (which
|
|
406
|
+
`lifecycle.js#resolveWorkItemScope` needs to route the completion back to the
|
|
407
|
+
session), routes an agent, derives the branch, honours the branch mutex, and
|
|
408
|
+
renders the phase playbook prompt.
|
|
409
|
+
|
|
410
|
+
Phase work items are `WORK_TYPE.TEST`, which is in
|
|
411
|
+
`shared.WORKTREE_REQUIRING_TYPES`, so their project association is
|
|
412
|
+
load-bearing: `spawnAgent` resolves the worktree rootDir from it, and without
|
|
413
|
+
one it falls back to MINIONS_DIR's parent — which collapses to a drive root on
|
|
414
|
+
installs where MINIONS_DIR sits one level below it (`D:\squad-opg` → `D:\`) and
|
|
415
|
+
fails non-retryably with `failure_class: worktree-preflight`. So
|
|
416
|
+
`_persistWorkItem` **binds** `wi.project` to the SQL scope the item is stored
|
|
417
|
+
in (a scope is `project.name` or the `central` sentinel) rather than trusting
|
|
418
|
+
the builders' `opts.project || spec.project || null` fallback chain, and a
|
|
419
|
+
project/scope disagreement throws instead of persisting an item the engine can
|
|
420
|
+
never place. Central scope has no owning project to bind: a declared project
|
|
421
|
+
there is preserved (that is how `discoverCentralWorkItems` still resolves a
|
|
422
|
+
rootDir), and a genuinely project-less session stays project-less — the
|
|
423
|
+
preflight is not masked.
|
|
424
|
+
|
|
213
425
|
## Endpoints
|
|
214
426
|
|
|
215
427
|
Documented in `dashboard.js`; routes are visible at `GET /api/routes`.
|
|
216
428
|
|
|
217
429
|
| Method | Path | Behavior |
|
|
218
430
|
|--------|--------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|
|
|
219
|
-
| POST | `/api/qa/session` | Create session; validates spec, calls `createSession` + `queueSetup` (`pending → spawning`). Body accepts `project: string` (single) OR `projects: string[]` (multi, ≤5;
|
|
220
|
-
| POST | `/api/qa/from-prd` | "qa this PRD" bridge. Build a QA Session spec from a completed/approved PRD via `engine/qa
|
|
221
|
-
| GET | `/api/qa/sessions` | List sessions newest-first. Optional `?limit=N` and `?state=pending\|spawning\|drafting\|awaiting-approval\|executing\|done\|failed\|killed`. |
|
|
431
|
+
| POST | `/api/qa/session` | Create session; validates spec, calls `createSession` + `queueSetup` (`pending → spawning`). Body accepts `project: string` (single) OR `projects: string[]` (multi, ≤5), plus the optional `primaryProject: string` (a member of `projects`; defaults to `projects[0]`) and `projectTargets: { [project]: target }` (per-project target overrides; each key a member of `projects`, each value validated like `target`). Returns `sessionId`, `setupWorkItemId` (primary's WI), `managedSpawnName`, and (multi only) `projects: string[]`, primary-first. |
|
|
432
|
+
| POST | `/api/qa/from-prd` | "qa this PRD" bridge. Build a QA Session spec from a completed/approved PRD via `engine/qa/from-prd.js` (pure) then reuse `createSession` + `queueSetup`. Body: `{ prd, mode?, runner? }`. Non-ready / unknown / malformed PRD → **400** (`QaFromPrdError.reason`). Returns `sessionId`, `state`, `setupWorkItemId`, `managedSpawnName`, `projects`, `flowSource`, `warnings[]`, `note` (repo-native-harness copy), and optional `runnerHint`. The test is drafted on the repo's native test harness. |
|
|
433
|
+
| GET | `/api/qa/sessions` | List sessions newest-first. Optional `?limit=N` and `?state=pending\|spawning\|drafting\|awaiting-approval\|executing\|done\|failed\|killed`. Each record carries a bounded `linkedRun` projection — `{ id, status, artifacts: [{type, path, label}] }` for a session with a `qaRunId` (primary image/video artifacts only, capped at `QA_PRIMARY_ARTIFACT_DEFAULT_MAX`), else `null`. Built from ONE `listRuns` read indexed by id, and fully try/catch'd: a deleted run or an unreadable runs store degrades to `linkedRun: null`, never a 500. |
|
|
222
434
|
| GET | `/api/qa/sessions/<id>` | Fetch a single session record by id. |
|
|
223
435
|
| POST | `/api/qa/sessions/<id>/approve` | `awaiting-approval → executing`. Accepts no request body. Server-side creates the linked `qa-runs` record (synthetic `runbookId='qa-session-<id>'`), queues EXECUTE WI, stamps `qaRunId` on the session. |
|
|
224
436
|
| POST | `/api/qa/sessions/<id>/edit` | `awaiting-approval → drafting`. Body: `{ feedback }`. Re-fires DRAFT with the reviewer feedback threaded into the prompt. |
|
|
225
437
|
| POST | `/api/qa/sessions/<id>/cancel` | Non-terminal → `killed`. Optional `{ reason }`. Does NOT touch the managed-spawn — use `/kill` for that. |
|
|
226
438
|
| POST | `/api/qa/sessions/<id>/kill` | Non-terminal → `killed`, then generation-safely removes the primary and every multi-project co-service managed-spawn. Best-effort when a spawn is absent or concurrently replaced. |
|
|
227
439
|
| POST | `/api/qa/sessions/<id>/dismiss` | Non-terminal → `done`. Accept the draft as final; leaves spawn alive. Optional `{ summary }`. |
|
|
440
|
+
| DELETE | `/api/qa/sessions/<id>` | Hard-delete a terminal session record (`done`/`failed`/`killed` only). Accepts no request body. Returns `{ ok: true, id, state, testDirRemoved, qaRun, managedSpawnsLive, managedSpawnsKnown }`. `409 not_terminal` (with `currentState`) for an in-flight session, `404 not_found` for unknown ids, `400` for an unsafe id. See "Deleting sessions". |
|
|
228
441
|
| GET | `/api/qa/runners` | List registered runner adapters (built-ins + `qa-runners.d/` plugins). Metadata only — hooks (functions) are stripped. |
|
|
229
442
|
| POST | `/api/qa/runners/reload` | Accepts no request body. Clears the in-process registry, re-registers built-ins, re-scans `qa-runners.d/` for plugin edits, and returns the fresh runner list. |
|
|
230
|
-
| DELETE | `/api/qa/runs/<id>` | Hard-delete a single QA run record (terminal-status runs only) and best-effort wipe its artifact directory under `engine/qa-artifacts/<runId>/`. Returns `{ ok: true, id, artifactsRemoved }` on success, `409 not_terminal` for in-flight runs, `404 not_found` for unknown ids. |
|
|
443
|
+
| DELETE | `/api/qa/runs/<id>` | Hard-delete a single QA run record (terminal-status runs only) and best-effort wipe its artifact directory under `engine/qa-artifacts/<runId>/`. Returns `{ ok: true, id, artifactsRemoved, deletedAs }` on success, `409 not_terminal` for in-flight runs, `404 not_found` for unknown ids. `?orphaned=1` additionally removes a provably-orphaned abandoned pending run, or returns `409 not_orphaned` with the failed condition in `reason`. |
|
|
231
444
|
|
|
232
445
|
The audited contracts returned by `GET /api/routes` expose the canonical enum,
|
|
233
446
|
length, list, nested-field, path, and lifecycle constraints for these routes.
|
|
@@ -243,6 +456,38 @@ errors are mapped to HTTP via `_qaSessionsErrorToStatus`:
|
|
|
243
456
|
- `'unsafe sessionId'` / `'invalid spec'` / `'requires …'` / `'exceeds …'` (spec or feedback validation) → 400
|
|
244
457
|
- `'illegal state transition'` / `'requires state …'` / `'requires non-terminal'` → 409
|
|
245
458
|
|
|
459
|
+
## Deleting sessions
|
|
460
|
+
|
|
461
|
+
`DELETE /api/qa/sessions/<id>` (`engine/qa/sessions.js#deleteSession`) exists so
|
|
462
|
+
an operator can clear failed/completed/killed rows out of the dashboard's
|
|
463
|
+
"Recent Sessions" list without hand-editing `engine/state.db`. It is the session
|
|
464
|
+
counterpart of `DELETE /api/qa/runs/<id>` and follows the same shape: the record
|
|
465
|
+
is removed inside `shared.mutateQaSessions` (which emits the `qa_sessions` state
|
|
466
|
+
event), and every filesystem / cross-store side effect runs outside the lock.
|
|
467
|
+
|
|
468
|
+
**Terminal-only.** Only `done`, `failed`, and `killed` are deletable. Any other
|
|
469
|
+
state returns `409 { error: 'not_terminal', currentState }` — cancel or kill the
|
|
470
|
+
session first (`/cancel`, `/kill`), then delete. A refused delete touches no
|
|
471
|
+
linked resource.
|
|
472
|
+
|
|
473
|
+
**Linked resources** are cleaned up only where ownership is unambiguous:
|
|
474
|
+
|
|
475
|
+
| Resource | Behavior |
|
|
476
|
+
|----------|----------|
|
|
477
|
+
| `engine/qa-tests/<id>/` | Removed. The directory is session-owned by construction (`createSession` creates it and the directory name *is* the session id), and removal repeats the `path.resolve` + prefix sandbox used for artifact directories, so only the per-session directory can be reclaimed — never the `qa-tests` root. Reported as `testDirRemoved`. |
|
|
478
|
+
| `session.qaRunId` | Cascaded through `qaRuns.deleteQaRun` **only** when the run still exists and carries the synthetic `runbookId === 'qa-session-<id>'` stamped by `/approve` and the auto-mode chain. Otherwise left in place, with the reason reported in `qaRun.reason`: `no_linked_run`, `not_found` (already deleted), `not_session_owned` (a real runbook owns it), `not_terminal` (+ `currentStatus` — a live run is never vacuumed out from under its agent), `lookup_failed`, or `delete_failed`. `deleteQaRun`'s own terminal-only guard is the second line of defence. |
|
|
479
|
+
| Managed-spawn processes | **Never killed.** A QA managed-spawn can outlive its session, so killing a live process as a side effect of deleting a history record would be a surprise. Live spawn names are reported in `managedSpawnsLive` (with `managedSpawnsKnown: false` when the managed-process store could not be read, so an empty list is not mistaken for "nothing live"); kill them deliberately via `POST /api/managed-processes/kill`. |
|
|
480
|
+
| Work items | Untouched. The SETUP/DRAFT/EXECUTE work items are dispatch history and are not owned by the session record. |
|
|
481
|
+
|
|
482
|
+
**Repeat and missing-link behavior is safe.** Deleting twice returns a plain
|
|
483
|
+
`404 not_found`; a linked run that a previous call (or a manual
|
|
484
|
+
`DELETE /api/qa/runs/<id>`) already removed reports
|
|
485
|
+
`qaRun.reason: 'not_found'` instead of failing the request. Note that removing a
|
|
486
|
+
session also removes the `session_owned` evidence
|
|
487
|
+
`evaluateOrphanedPendingRun` consults, so a still-`pending` run left behind
|
|
488
|
+
becomes eligible for the explicit `DELETE /api/qa/runs/<id>?orphaned=1` escape
|
|
489
|
+
hatch once its other conditions hold.
|
|
490
|
+
|
|
246
491
|
## File locations
|
|
247
492
|
|
|
248
493
|
- **Session state**: `engine/state.db` `qa_sessions` (all projects,
|
|
@@ -262,7 +507,7 @@ errors are mapped to HTTP via `_qaSessionsErrorToStatus`:
|
|
|
262
507
|
|
|
263
508
|
## Runner adapters (P-c4a9e7f3 / P-b8e1d4a6)
|
|
264
509
|
|
|
265
|
-
Pluggable test-runner registry at `engine/qa-runners
|
|
510
|
+
Pluggable test-runner registry at `engine/qa-runners/`. Built-in
|
|
266
511
|
adapters: `playwright` (priority 50, always-true safe default),
|
|
267
512
|
`maestro` (priority 80, detects `.maestro/` and therefore wins when
|
|
268
513
|
present). Each adapter exports five hooks:
|
|
@@ -282,7 +527,7 @@ present). Each adapter exports five hooks:
|
|
|
282
527
|
Resolution order in `detectRunner(target, project, explicitRunner)`:
|
|
283
528
|
explicit-name (no `detect` call, unknown names return null), then
|
|
284
529
|
priority-desc iteration. Plugin folder: `<MINIONS_DIR>/qa-runners.d/*.js`
|
|
285
|
-
(same trust level as `playbooks/` and `
|
|
530
|
+
(same trust level as `playbooks/` and `watch-plugins/`). Hot-reload via
|
|
286
531
|
`POST /api/qa/runners/reload` (clears registry → re-registers built-ins →
|
|
287
532
|
re-scans plugin dir) so plugin edits take effect without an engine
|
|
288
533
|
restart.
|
|
@@ -290,7 +535,7 @@ restart.
|
|
|
290
535
|
## Fast-state slice
|
|
291
536
|
|
|
292
537
|
`/api/status.qaSessions = { total, sig }` — the unsorted summary helper
|
|
293
|
-
`engine/qa
|
|
538
|
+
`engine/qa/sessions.js#summarizeSessionsForStatus()`. Mirrors `qaRuns` so
|
|
294
539
|
the sidebar activity-dot lights up on any new session or state
|
|
295
540
|
transition within one `/api/status` poll cycle (~4s). Do NOT call
|
|
296
541
|
`listSessions({limit:50})` from this hot path — it sorts O(N log N) on
|
|
@@ -310,6 +555,18 @@ every fast-state rebuild.
|
|
|
310
555
|
chip classes (`--done` / `--active` / `--pending` / `--failed` /
|
|
311
556
|
`--killed`) per `session.state`. State-driven left-border color
|
|
312
557
|
(red=failed, green=done, yellow=awaiting-approval, blue=active).
|
|
558
|
+
- **Linked run + evidence** (W-msb9kgs402813a97-d) — a card carrying a
|
|
559
|
+
`qaRunId` renders a `run:` chip in the meta row that scrolls the matching
|
|
560
|
+
Recent Runs row (`#qa-run-<id>`) into view, and a **terminal** card renders
|
|
561
|
+
the run's primary (image/video) artifacts through the same
|
|
562
|
+
`_qaRenderArtifactPreviews` helper the Recent Runs strip uses — so artifact
|
|
563
|
+
URLs always go through `/api/qa/artifacts/<runId>/<path>` and no filesystem
|
|
564
|
+
path is ever exposed. A terminal card whose run captured nothing says
|
|
565
|
+
"No screenshots or video captured for this run." rather than rendering
|
|
566
|
+
nothing: a session that captured plenty and one that captured nothing used
|
|
567
|
+
to be indistinguishable. The data comes from the bounded `linkedRun`
|
|
568
|
+
projection on `GET /api/qa/sessions` (see the endpoint table), not from the
|
|
569
|
+
Recent Runs poll, so card content does not depend on poll ordering.
|
|
313
570
|
- **Action buttons** —
|
|
314
571
|
`awaiting-approval` cards show `[Approve & run]` `[Edit]` `[Cancel]`;
|
|
315
572
|
every non-terminal card shows `[Dismiss]` `[Kill spawn]` in the footer;
|
|
@@ -338,15 +595,36 @@ field gets the right audit trail.
|
|
|
338
595
|
|
|
339
596
|
A completed/approved PRD can be QA'd in one shot — no hand-built spec. The
|
|
340
597
|
bridge is `POST /api/qa/from-prd`, which turns the PRD into a QA Session
|
|
341
|
-
spec via the **pure** spec-builder `engine/qa
|
|
598
|
+
spec via the **pure** spec-builder `engine/qa/from-prd.js`
|
|
342
599
|
(`buildQaSessionSpecFromPrd`) and then reuses the **exact same**
|
|
343
600
|
orchestration as `POST /api/qa/session` (`createSession` + `queueSetup`,
|
|
344
601
|
`pending → spawning`). No forked dispatch logic — the SETUP → DRAFT →
|
|
345
602
|
EXECUTE chain above runs verbatim.
|
|
346
603
|
|
|
604
|
+
That orchestration lives in **`engine/qa/prd-session.js`**
|
|
605
|
+
(`createQaSessionFromPrd`), not in the HTTP handler, so the automatic
|
|
606
|
+
post-verify path below creates identical sessions instead of forking the
|
|
607
|
+
route. The handler keeps only what is genuinely HTTP-specific (body
|
|
608
|
+
validation, header identity, status-code mapping, the runner hint). The one
|
|
609
|
+
injected seam is `resolveTargets`: the dashboard runs each project name
|
|
610
|
+
through its HTTP-input validator (bad project → 400 with an `ApiInputError`
|
|
611
|
+
path), while the engine uses `resolveTargetsFromConfig`, which walks the same
|
|
612
|
+
`shared.resolveProjectSource` seam against the live config.
|
|
613
|
+
|
|
614
|
+
Both resolvers stamp the caller's `requested` name next to the canonical
|
|
615
|
+
`project` and pipe the list through the shared
|
|
616
|
+
`orderTargetsByPrimary(spec, targets)`, so a spec's `primaryProject` leads the
|
|
617
|
+
resolved list and every entry carries a `primary` boolean. That lets
|
|
618
|
+
`canonicalizeResolvedProjects` rewrite `primaryProject` and the
|
|
619
|
+
`projectTargets` keys to canonical project names — without it, a PRD naming
|
|
620
|
+
`"Web"` would produce `projects: ["web"]` alongside `primaryProject: "Web"` and
|
|
621
|
+
`validateSpec` would reject a spec the caller wrote correctly.
|
|
622
|
+
|
|
347
623
|
**Body:** `{ prd: "<file>.json", mode?, runner? }`. `prd` is a PRD filename
|
|
348
|
-
resolved
|
|
349
|
-
|
|
624
|
+
resolved against the SQL PRD store (live bucket, then the archive bucket),
|
|
625
|
+
with a legacy `prd/` + `prd/archive/` filesystem fallback for pre-migration
|
|
626
|
+
installs. Traversal / absolute / drive-letter shapes are rejected. `mode` and
|
|
627
|
+
`runner` are optional overrides.
|
|
350
628
|
|
|
351
629
|
**Completed/approved guard rail.** The builder rejects any PRD whose
|
|
352
630
|
`status` is not in `{completed, approved}` (`QA_READY_STATUSES`) — drafting
|
|
@@ -407,6 +685,59 @@ alongside the usual `sessionId` / `state` / `setupWorkItemId` /
|
|
|
407
685
|
Command Center exposes this as a natural-language shortcut — "qa this PRD"
|
|
408
686
|
maps to `POST /api/qa/from-prd` (see `prompts/cc-system.md`).
|
|
409
687
|
|
|
688
|
+
## Automatic QA for verified PRDs (`engine.autoQaCompletedPrds`, W-msal3ser01q4ccb3)
|
|
689
|
+
|
|
690
|
+
`engine.autoQaCompletedPrds` (default **false**, Settings → Plan & PR
|
|
691
|
+
Workflow → Plan) makes the engine start a PRD-driven QA Session by itself.
|
|
692
|
+
Resolved through `shared.resolveAutoQaCompletedPrds(engine)`; only an explicit
|
|
693
|
+
boolean counts, so a malformed `config.json` cannot silently enable an
|
|
694
|
+
automatic dispatch. When OFF, behaviour is byte-identical to today and the
|
|
695
|
+
manual endpoint above is unaffected either way.
|
|
696
|
+
|
|
697
|
+
**Trigger — verification, not the status flip.** The hook lives in
|
|
698
|
+
`engine/qa/auto-prd-qa.js#maybeAutoQaAfterVerify` and fires from
|
|
699
|
+
`engine/orchestration/lifecycle.js#runPostCompletionHooks` on a **successful,
|
|
700
|
+
non-skipped verify work item**. It deliberately does *not* key off the PRD's
|
|
701
|
+
top-level `status: completed`: `checkPlanCompletion` sets that flag **before**
|
|
702
|
+
creating the verify work item(s), so triggering there would race the verify
|
|
703
|
+
agent and miss the canonical guide. All four gates must hold:
|
|
704
|
+
|
|
705
|
+
1. `engine.autoQaCompletedPrds` is on.
|
|
706
|
+
2. The completing work item is a `verify` type with a `sourcePlan`.
|
|
707
|
+
3. `prd/guides/verify-<plan>.md` exists — the artifact that proves
|
|
708
|
+
verification ran to completion. Flow *priority* is untouched: the builder
|
|
709
|
+
still prefers the verify guide, then `manual-qa-<plan>.md`, then
|
|
710
|
+
acceptance criteria.
|
|
711
|
+
4. **Every** verify work item for that plan (across all project scopes) is in
|
|
712
|
+
a `DONE_STATUSES` state. A cross-repo plan therefore waits for its whole
|
|
713
|
+
per-project fan-out and launches **one** coherent session — not one per
|
|
714
|
+
verify completion. Any `failed` / `cancelled` / still-pending sibling
|
|
715
|
+
blocks QA entirely.
|
|
716
|
+
|
|
717
|
+
Automatic sessions use `mode: 'auto'` (SETUP → DRAFT → EXECUTE with no
|
|
718
|
+
approval step — nobody is standing by to approve the draft) and
|
|
719
|
+
`createdBy: 'engine:auto-qa-verified-prd'`. Their provenance carries an extra
|
|
720
|
+
`autoTrigger: { reason: 'verified-prd', generation, verifyWorkItemIds[],
|
|
721
|
+
triggeredBy, at }` block alongside the usual PRD provenance.
|
|
722
|
+
|
|
723
|
+
**Idempotency — a durable verify generation, not an in-memory flag.** The
|
|
724
|
+
dedupe key is `sha256(planFile + every verify WI's "id@completedAt")`,
|
|
725
|
+
truncated to 16 chars. It is CLAIMED inside a `prdStore.mutatePrd` transaction
|
|
726
|
+
(compare-and-set) before any session is created and stamped on the PRD row as
|
|
727
|
+
`_autoQa.launches[]` (bounded to the last 5 entries), so it survives ticks,
|
|
728
|
+
engine restarts, lifecycle retries, duplicate completion callbacks, and
|
|
729
|
+
concurrent cross-repo completions landing in the same tick. `completedAt` is
|
|
730
|
+
what makes a **reopened** PRD re-verifiable: `shared.reopenWorkItem` drops it,
|
|
731
|
+
so the next successful completion produces a different generation and may
|
|
732
|
+
legitimately launch a new session — while the *same* generation always
|
|
733
|
+
collapses to exactly one.
|
|
734
|
+
|
|
735
|
+
**Failure is bounded and never regresses the PRD.** A rejected launch records
|
|
736
|
+
`status: 'error'` on the launch entry and **keeps** the claim; not releasing it
|
|
737
|
+
is what stops a broken QA path from re-firing every tick. The hook never
|
|
738
|
+
throws (every failure degrades to a warn), never writes PRD completion state,
|
|
739
|
+
and QA failure stays visible in QA state only.
|
|
740
|
+
|
|
410
741
|
## When something goes wrong
|
|
411
742
|
|
|
412
743
|
- **SETUP managed-spawn won't validate** → session lands in `failed` with
|
|
@@ -419,10 +750,29 @@ maps to `POST /api/qa/from-prd` (see `prompts/cc-system.md`).
|
|
|
419
750
|
the injected
|
|
420
751
|
`engine/qa-tests/<sessionId>/` directory. The session failure class is
|
|
421
752
|
`qa-session-draft-failed` when the agent reports that contract failure.
|
|
753
|
+
- **DRAFT succeeded but the session failed anyway** → `failure_class:
|
|
754
|
+
'qa-session-draft-visual-coverage'`. The session asked for screenshots or
|
|
755
|
+
video and the agent's visual-journey manifest
|
|
756
|
+
(`agents/<agentId>/qa-session-draft-result.json`, see
|
|
757
|
+
`engine/qa/visual-journey.js`) was missing, API-only, or did not cover every
|
|
758
|
+
project / managed-spawn / requested capture type. `session.error` names
|
|
759
|
+
exactly what is missing; POST `/api/qa/sessions/<id>/edit` is not available
|
|
760
|
+
(the session is terminal), so fix the flows/capture spec and create a new
|
|
761
|
+
session, or re-run DRAFT via a new session with the same target.
|
|
422
762
|
- **EXECUTE qa-run terminal status is `failed`/`errored`** →
|
|
423
763
|
`failure_class: 'qa-session-execute-failed'` /
|
|
424
764
|
`'qa-session-execute-errored'`. The linked `qa-runs` record (joined via
|
|
425
765
|
`session.qaRunId`) carries the agent's `summary` and artifact list.
|
|
766
|
+
- **EXECUTE qa-run passed but the session failed anyway** → `failure_class:
|
|
767
|
+
'qa-session-evidence-incomplete'`. The session asked for screenshots or video
|
|
768
|
+
and `validateEvidenceCoverage` could not prove the evidence: a requested
|
|
769
|
+
capture type had no registered artifact, a registered artifact was missing /
|
|
770
|
+
zero-byte / outside `engine/qa-artifacts/<runId>/`, a project or managed-spawn
|
|
771
|
+
appeared in no **passed** `journeyCoverage` entry, or a journey the DRAFT
|
|
772
|
+
manifest declared never reported `passed`. `session.error` names exactly what
|
|
773
|
+
was missing; the linked run keeps its own `passed` status, so compare
|
|
774
|
+
`run.artifacts` + `run.journeyCoverage` against
|
|
775
|
+
`session.visualJourneyManifest` to see the gap.
|
|
426
776
|
- **Want to start over after seeing a bad draft** → POST
|
|
427
777
|
`/api/qa/sessions/<id>/edit` with `{ feedback: "…" }`; do NOT
|
|
428
778
|
`/cancel` + create a new session unless the original spec was wrong
|
package/docs/qa-runbooks.md
CHANGED
|
@@ -16,7 +16,7 @@ scoped to a single project lives under its `projects/<name>/` state dir
|
|
|
16
16
|
rather than a root-level `runbooks/` directory. Two reasons:
|
|
17
17
|
|
|
18
18
|
1. **Lifecycle parity with the project.** When a project is removed via
|
|
19
|
-
`engine/projects.js removeProject`, its `projects/<name>/` dir is
|
|
19
|
+
`engine/planning/projects.js removeProject`, its `projects/<name>/` dir is
|
|
20
20
|
archived as one unit. Co-locating runbooks under that dir means they
|
|
21
21
|
travel with the project rather than dangling in a global `runbooks/`
|
|
22
22
|
that has no relationship to the project being removed.
|
|
@@ -76,7 +76,7 @@ Responses:
|
|
|
76
76
|
|
|
77
77
|
## Module
|
|
78
78
|
|
|
79
|
-
`engine/qa
|
|
79
|
+
`engine/qa/runbooks.js` exports:
|
|
80
80
|
|
|
81
81
|
```js
|
|
82
82
|
{
|
|
@@ -99,7 +99,7 @@ unlink).
|
|
|
99
99
|
|
|
100
100
|
The deferred follow-up items (W-mpeiwz6k0005bf34-b/c/d) have since landed. Brief pointers — see [CLAUDE.md](../CLAUDE.md) → "QA validation runs" for the deep dive:
|
|
101
101
|
|
|
102
|
-
- **Run dispatch + persistence** (`engine/qa
|
|
102
|
+
- **Run dispatch + persistence** (`engine/qa/runs.js`): `POST /api/qa/runbooks/run` creates a SQL QA-run record with `status ∈ pending|running|passed|failed|errored` and dispatches a `qa-validate` work item against the runbook's `targetName`. Read via `GET /api/qa/runs?limit=N&status=...` and `GET /api/qa/runs/<id>`.
|
|
103
103
|
- **Artifact contract**: the engine pre-creates `engine/qa-artifacts/<runId>/`; the `qa-validate` agent writes files there and records run-root-relative paths in the injected absolute `agents/<id>/qa-run-result.json` path. The engine clears that sidecar before dispatch, verifies its `runId`, and removes it after consumption. Lifecycle persists the metadata, and `GET /api/qa/artifacts/<runId>/<path>` serves root-level or nested files behind traversal guards (403 on escape). There is no `engine.qaArtifactsMaxBytes` setting or copy-on-completion gate.
|
|
104
104
|
- **UI**: `/qa` dashboard page (`dashboard/pages/qa.html`, `dashboard/js/qa.js`) polls `GET /api/qa/runs` every 5s while active; auto-detects screenshots/videos/logs for inline preview.
|
|
105
105
|
- **Playbook**: `playbooks/qa-validate.md` (routed via the synthetic `qa-validate` task-type in `routing.md`).
|