@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
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
const fs = require('fs');
|
|
30
30
|
const path = require('path');
|
|
31
|
-
const shared = require('
|
|
31
|
+
const shared = require('../core/shared');
|
|
32
32
|
const { mutateQaRuns, uid, ts, log } = shared;
|
|
33
33
|
|
|
34
34
|
// Cap qa_runs so the table doesn't grow unboundedly
|
|
@@ -42,6 +42,13 @@ const QA_ARTIFACT_PATH_MAX = 2048;
|
|
|
42
42
|
const QA_ARTIFACT_SEGMENT_MAX = 255;
|
|
43
43
|
const QA_RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
44
44
|
|
|
45
|
+
// W-msb9kgs402813a97-c — journey coverage reported by the EXECUTE agent in its
|
|
46
|
+
// qa-run-result sidecar. Caps mirror the artifact segment cap so a runaway
|
|
47
|
+
// agent cannot inflate the run's `data` blob.
|
|
48
|
+
const QA_JOURNEY_COVERAGE_MAX = 50;
|
|
49
|
+
const QA_JOURNEY_COVERAGE_LIST_MAX = 50;
|
|
50
|
+
const QA_JOURNEY_COVERAGE_STATUSES = Object.freeze(new Set(['passed', 'failed', 'skipped']));
|
|
51
|
+
|
|
45
52
|
const QA_RUN_STATUS = Object.freeze({
|
|
46
53
|
PENDING: 'pending',
|
|
47
54
|
RUNNING: 'running',
|
|
@@ -59,7 +66,7 @@ const TERMINAL_STATUSES = new Set([
|
|
|
59
66
|
// Allowed forward transitions. Anything not enumerated here is rejected.
|
|
60
67
|
//
|
|
61
68
|
// PR #2697 review fix (W-mpeiwz6k0005bf34-c — Ripley): the lifecycle hook in
|
|
62
|
-
// engine/lifecycle.js parses the agent's qa-run-result.json sidecar and calls
|
|
69
|
+
// engine/orchestration/lifecycle.js parses the agent's qa-run-result.json sidecar and calls
|
|
63
70
|
// completeRun({status: 'passed'|'failed'|'errored'}) directly. It never calls
|
|
64
71
|
// markRunning, because the agent may crash before writing the sidecar (in
|
|
65
72
|
// which case the hook still needs to mark the run errored from `pending`).
|
|
@@ -183,17 +190,133 @@ function validateTransition(from, to) {
|
|
|
183
190
|
}
|
|
184
191
|
}
|
|
185
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Normalize one artifact record from the agent's qa-run-result sidecar.
|
|
195
|
+
*
|
|
196
|
+
* `journeyId` and `project` (W-msb9kgs402813a97-c) are OPTIONAL and only
|
|
197
|
+
* persisted when the agent actually declared them, so legacy qa-validate runs
|
|
198
|
+
* keep their historical record shape instead of gaining empty keys.
|
|
199
|
+
*/
|
|
186
200
|
function normalizeArtifact(a) {
|
|
187
201
|
if (!a || typeof a !== 'object') return null;
|
|
188
202
|
const type = typeof a.type === 'string' ? a.type : '';
|
|
189
203
|
const p = typeof a.path === 'string' ? a.path : '';
|
|
190
204
|
if (!type || !p) return null;
|
|
191
|
-
|
|
205
|
+
const out = {
|
|
192
206
|
type,
|
|
193
207
|
path: p,
|
|
194
208
|
label: typeof a.label === 'string' ? a.label : '',
|
|
195
209
|
capturedAt: typeof a.capturedAt === 'string' && a.capturedAt ? a.capturedAt : ts(),
|
|
196
210
|
};
|
|
211
|
+
const journeyId = _normalizeCoverageName(a.journeyId);
|
|
212
|
+
if (journeyId) out.journeyId = journeyId;
|
|
213
|
+
const project = _normalizeCoverageName(a.project);
|
|
214
|
+
if (project) out.project = project;
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function _normalizeCoverageName(value) {
|
|
219
|
+
if (typeof value !== 'string') return '';
|
|
220
|
+
const trimmed = value.trim();
|
|
221
|
+
if (!trimmed || trimmed.length > QA_ARTIFACT_SEGMENT_MAX) return '';
|
|
222
|
+
return trimmed;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function _normalizeCoverageList(value) {
|
|
226
|
+
if (!Array.isArray(value)) return [];
|
|
227
|
+
const out = [];
|
|
228
|
+
for (const entry of value) {
|
|
229
|
+
const name = _normalizeCoverageName(entry);
|
|
230
|
+
if (name && !out.includes(name)) out.push(name);
|
|
231
|
+
if (out.length >= QA_JOURNEY_COVERAGE_LIST_MAX) break;
|
|
232
|
+
}
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Normalize the sidecar's `journeyCoverage` array — what the run actually
|
|
238
|
+
* exercised, per journey declared by the DRAFT visual-journey manifest.
|
|
239
|
+
* Malformed entries are DROPPED rather than repaired: a coverage claim the
|
|
240
|
+
* engine cannot read is not a coverage claim, and the EXECUTE evidence gate
|
|
241
|
+
* (engine/qa/visual-journey.js#validateEvidenceCoverage) then fails closed on
|
|
242
|
+
* the journey it can no longer prove.
|
|
243
|
+
*
|
|
244
|
+
* @param {Array} entries
|
|
245
|
+
* @returns {Array<{journeyId:string,status:string,projects:string[],services:string[]}>}
|
|
246
|
+
*/
|
|
247
|
+
function normalizeJourneyCoverage(entries) {
|
|
248
|
+
if (!Array.isArray(entries)) return [];
|
|
249
|
+
const out = [];
|
|
250
|
+
const seen = new Set();
|
|
251
|
+
for (const entry of entries) {
|
|
252
|
+
if (out.length >= QA_JOURNEY_COVERAGE_MAX) break;
|
|
253
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
|
|
254
|
+
const journeyId = _normalizeCoverageName(entry.journeyId);
|
|
255
|
+
if (!journeyId || seen.has(journeyId)) continue;
|
|
256
|
+
const status = typeof entry.status === 'string' ? entry.status : '';
|
|
257
|
+
if (!QA_JOURNEY_COVERAGE_STATUSES.has(status)) continue;
|
|
258
|
+
seen.add(journeyId);
|
|
259
|
+
out.push({
|
|
260
|
+
journeyId,
|
|
261
|
+
status,
|
|
262
|
+
projects: _normalizeCoverageList(entry.projects),
|
|
263
|
+
services: _normalizeCoverageList(entry.services),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// W-msb9kgs402813a97-d — "primary" artifacts are the ones that ARE the visible
|
|
270
|
+
// result of a QA run: screenshots and video. Everything else (logs, json, md,
|
|
271
|
+
// traces) is supporting material. This classifier lives here because runs.js
|
|
272
|
+
// owns the artifact contract (see normalizeArtifact above); the /qa Recent Runs
|
|
273
|
+
// strip applies the same partition client-side in
|
|
274
|
+
// dashboard/js/qa.js#_qaRenderArtifactPreviews — keep the two extension lists
|
|
275
|
+
// in sync, same convention as _qaIsTerminalStatus ↔ TERMINAL_STATUSES.
|
|
276
|
+
const PRIMARY_IMAGE_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg']);
|
|
277
|
+
const PRIMARY_VIDEO_EXTS = new Set(['mp4', 'webm', 'ogg', 'mov']);
|
|
278
|
+
|
|
279
|
+
// A session card is a summary, not a gallery — a handful of previews is enough
|
|
280
|
+
// to answer "did this run capture anything?" without turning a 50-session poll
|
|
281
|
+
// response into a payload problem.
|
|
282
|
+
const QA_PRIMARY_ARTIFACT_DEFAULT_MAX = 4;
|
|
283
|
+
|
|
284
|
+
function isPrimaryArtifact(a) {
|
|
285
|
+
if (!a || typeof a !== 'object') return false;
|
|
286
|
+
const p = typeof a.path === 'string' ? a.path : '';
|
|
287
|
+
if (!p) return false;
|
|
288
|
+
const type = String(a.type || '').toLowerCase();
|
|
289
|
+
if (type === 'screenshot' || type === 'image' || type === 'video') return true;
|
|
290
|
+
const ext = (p.match(/\.([a-z0-9]+)$/i) || ['', ''])[1].toLowerCase();
|
|
291
|
+
return PRIMARY_IMAGE_EXTS.has(ext) || PRIMARY_VIDEO_EXTS.has(ext);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Project a run's artifacts down to the bounded primary set a summary surface
|
|
296
|
+
* can render. Order is preserved; the return shape is exactly
|
|
297
|
+
* { type, path, label } so no internal bookkeeping (capturedAt) leaks into an
|
|
298
|
+
* API payload. Never throws on hostile input.
|
|
299
|
+
*
|
|
300
|
+
* @param {Array<object>} artifacts
|
|
301
|
+
* @param {number} [limit=QA_PRIMARY_ARTIFACT_DEFAULT_MAX]
|
|
302
|
+
* @returns {Array<{type: string, path: string, label: string}>}
|
|
303
|
+
*/
|
|
304
|
+
function selectPrimaryArtifacts(artifacts, limit = QA_PRIMARY_ARTIFACT_DEFAULT_MAX) {
|
|
305
|
+
if (!Array.isArray(artifacts)) return [];
|
|
306
|
+
const cap = Number.isFinite(limit) && limit > 0
|
|
307
|
+
? Math.floor(limit)
|
|
308
|
+
: QA_PRIMARY_ARTIFACT_DEFAULT_MAX;
|
|
309
|
+
const out = [];
|
|
310
|
+
for (const a of artifacts) {
|
|
311
|
+
if (out.length >= cap) break;
|
|
312
|
+
if (!isPrimaryArtifact(a)) continue;
|
|
313
|
+
out.push({
|
|
314
|
+
type: typeof a.type === 'string' ? a.type : '',
|
|
315
|
+
path: a.path,
|
|
316
|
+
label: typeof a.label === 'string' ? a.label : '',
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
197
320
|
}
|
|
198
321
|
|
|
199
322
|
function createRunId() {
|
|
@@ -303,10 +426,13 @@ function markRunning(id) {
|
|
|
303
426
|
* @param {object} opts
|
|
304
427
|
* @param {string} opts.status - 'passed' | 'failed' | 'errored'
|
|
305
428
|
* @param {string} [opts.summary] - human-readable summary
|
|
306
|
-
* @param {Array<object>} [opts.artifacts] - artifact records {type, path, label?, capturedAt?}
|
|
429
|
+
* @param {Array<object>} [opts.artifacts] - artifact records {type, path, label?, capturedAt?, journeyId?, project?}
|
|
430
|
+
* @param {Array<object>} [opts.journeyCoverage] - per-journey outcome records
|
|
431
|
+
* {journeyId, status, projects[], services[]}. Omit entirely for legacy
|
|
432
|
+
* qa-validate runs — the field is only stamped when the caller supplies it.
|
|
307
433
|
* @returns {object} updated run
|
|
308
434
|
*/
|
|
309
|
-
function completeRun(id, { status, summary, artifacts } = {}) {
|
|
435
|
+
function completeRun(id, { status, summary, artifacts, journeyCoverage } = {}) {
|
|
310
436
|
if (!id) throw new Error('qa-runs: id is required');
|
|
311
437
|
if (!TERMINAL_STATUSES.has(status)) {
|
|
312
438
|
throw new Error(`qa-runs: completeRun status must be one of ${[...TERMINAL_STATUSES].join(', ')}`);
|
|
@@ -314,6 +440,9 @@ function completeRun(id, { status, summary, artifacts } = {}) {
|
|
|
314
440
|
const normalized = Array.isArray(artifacts)
|
|
315
441
|
? artifacts.map(normalizeArtifact).filter(Boolean)
|
|
316
442
|
: [];
|
|
443
|
+
const normalizedCoverage = Array.isArray(journeyCoverage)
|
|
444
|
+
? normalizeJourneyCoverage(journeyCoverage)
|
|
445
|
+
: null;
|
|
317
446
|
|
|
318
447
|
let captured = null;
|
|
319
448
|
let transitionError = null;
|
|
@@ -330,6 +459,7 @@ function completeRun(id, { status, summary, artifacts } = {}) {
|
|
|
330
459
|
if (!Array.isArray(run.artifacts)) run.artifacts = [];
|
|
331
460
|
run.artifacts.push(...normalized);
|
|
332
461
|
}
|
|
462
|
+
if (normalizedCoverage) run.journeyCoverage = normalizedCoverage;
|
|
333
463
|
captured = run;
|
|
334
464
|
return runs;
|
|
335
465
|
});
|
|
@@ -350,7 +480,7 @@ function getRun(id) {
|
|
|
350
480
|
}
|
|
351
481
|
|
|
352
482
|
function _readRuns() {
|
|
353
|
-
return require('
|
|
483
|
+
return require('../persistence/small-state-store').readQaRuns();
|
|
354
484
|
}
|
|
355
485
|
|
|
356
486
|
/**
|
|
@@ -436,36 +566,169 @@ function _isSafeRunId(id) {
|
|
|
436
566
|
return QA_RUN_ID_RE.test(id);
|
|
437
567
|
}
|
|
438
568
|
|
|
569
|
+
// W-msaklqb701jy584d — evidence-based cleanup for abandoned pending runs.
|
|
570
|
+
//
|
|
571
|
+
// deleteQaRun is terminal-only by design so an operator can't vacuum a live QA
|
|
572
|
+
// agent out from under itself. That left one class of record permanently
|
|
573
|
+
// undeletable through the API: a `pending` run created by a stray process,
|
|
574
|
+
// never dispatched, owning nothing, whose runbook and managed target don't
|
|
575
|
+
// exist. `evaluateOrphanedPendingRun` is the narrow escape hatch — EVERY
|
|
576
|
+
// condition below must be independently provable before
|
|
577
|
+
// deleteQaRun(id, { allowOrphanedPending: true }) will remove the record. Any
|
|
578
|
+
// unreadable ownership source fails CLOSED (not orphaned), because leaving a
|
|
579
|
+
// stale row costs nothing while deleting an owned one loses QA history.
|
|
580
|
+
const QA_ORPHAN_MIN_AGE_MS = 24 * 60 * 60 * 1000;
|
|
581
|
+
|
|
582
|
+
function _parseTsMs(value) {
|
|
583
|
+
if (typeof value !== 'string' || !value) return NaN;
|
|
584
|
+
const ms = Date.parse(value);
|
|
585
|
+
return Number.isFinite(ms) ? ms : NaN;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// { known } is false when the source could not be read at all — the caller
|
|
589
|
+
// must then refuse the delete rather than assume "no owner".
|
|
590
|
+
function _runSessionOwnership(id) {
|
|
591
|
+
try {
|
|
592
|
+
const sessions = require('./sessions').listSessions({});
|
|
593
|
+
if (!Array.isArray(sessions)) return { known: false, owned: false };
|
|
594
|
+
return { known: true, owned: sessions.some(s => s && s.qaRunId === id) };
|
|
595
|
+
} catch (_e) {
|
|
596
|
+
return { known: false, owned: false };
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function _runWorkItemOwnership(id) {
|
|
601
|
+
try {
|
|
602
|
+
const items = require('../planning/work-items-store').readAllWorkItems();
|
|
603
|
+
if (!Array.isArray(items)) return { known: false, owned: false };
|
|
604
|
+
return {
|
|
605
|
+
known: true,
|
|
606
|
+
owned: items.some(wi => wi && wi.meta && wi.meta.qaRunId === id),
|
|
607
|
+
};
|
|
608
|
+
} catch (_e) {
|
|
609
|
+
return { known: false, owned: false };
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Decide whether a run is a provably-orphaned abandoned pending record.
|
|
615
|
+
*
|
|
616
|
+
* @param {string} id - run id
|
|
617
|
+
* @param {object} [opts]
|
|
618
|
+
* @param {number} [opts.now] - epoch ms, injectable for tests
|
|
619
|
+
* @param {number} [opts.minAgeMs] - minimum age before a pending run qualifies
|
|
620
|
+
* @returns {{id: string, exists: boolean, status: string|null, orphaned: boolean,
|
|
621
|
+
* reason: string|null, ageMs?: number}}
|
|
622
|
+
* `reason` names the first condition that failed, and is null when orphaned.
|
|
623
|
+
*/
|
|
624
|
+
function evaluateOrphanedPendingRun(id, { now = Date.now(), minAgeMs = QA_ORPHAN_MIN_AGE_MS } = {}) {
|
|
625
|
+
const result = { id, exists: false, status: null, orphaned: false, reason: null };
|
|
626
|
+
if (!_isSafeRunId(id)) { result.reason = 'invalid_id'; return result; }
|
|
627
|
+
|
|
628
|
+
const run = getRun(id);
|
|
629
|
+
if (!run) { result.reason = 'not_found'; return result; }
|
|
630
|
+
result.exists = true;
|
|
631
|
+
result.status = run.status;
|
|
632
|
+
|
|
633
|
+
// Never touch an active run: `running` is live, and any pending run that
|
|
634
|
+
// already started, completed, captured artifacts, or carries a work item is
|
|
635
|
+
// owned, not abandoned.
|
|
636
|
+
if (run.status !== QA_RUN_STATUS.PENDING) { result.reason = 'status_not_pending'; return result; }
|
|
637
|
+
if (run.startedAt) { result.reason = 'already_started'; return result; }
|
|
638
|
+
if (run.completedAt) { result.reason = 'already_completed'; return result; }
|
|
639
|
+
if (Array.isArray(run.artifacts) && run.artifacts.length) { result.reason = 'has_artifacts'; return result; }
|
|
640
|
+
if (run.workItemId) { result.reason = 'work_item_owned'; return result; }
|
|
641
|
+
|
|
642
|
+
// Age gate: covers the window where a dispatch created the run and is still
|
|
643
|
+
// about to back-fill its work item / session linkage.
|
|
644
|
+
const createdMs = _parseTsMs(run.createdAt);
|
|
645
|
+
if (!Number.isFinite(createdMs)) { result.reason = 'unknown_age'; return result; }
|
|
646
|
+
result.ageMs = now - createdMs;
|
|
647
|
+
if (result.ageMs < minAgeMs) { result.reason = 'too_recent'; return result; }
|
|
648
|
+
|
|
649
|
+
const session = _runSessionOwnership(id);
|
|
650
|
+
if (!session.known) { result.reason = 'session_ownership_unknown'; return result; }
|
|
651
|
+
if (session.owned) { result.reason = 'session_owned'; return result; }
|
|
652
|
+
|
|
653
|
+
const workItem = _runWorkItemOwnership(id);
|
|
654
|
+
if (!workItem.known) { result.reason = 'work_item_ownership_unknown'; return result; }
|
|
655
|
+
if (workItem.owned) { result.reason = 'work_item_owned'; return result; }
|
|
656
|
+
|
|
657
|
+
let runbook;
|
|
658
|
+
try { runbook = require('./runbooks').getRunbook(run.runbookId); }
|
|
659
|
+
catch (_e) { result.reason = 'runbook_lookup_failed'; return result; }
|
|
660
|
+
if (runbook) { result.reason = 'runbook_exists'; return result; }
|
|
661
|
+
|
|
662
|
+
let target;
|
|
663
|
+
try { target = require('../processes/managed-spawn').getManagedSpecByName(run.targetName); }
|
|
664
|
+
catch (_e) { result.reason = 'target_lookup_failed'; return result; }
|
|
665
|
+
if (target) { result.reason = 'target_exists'; return result; }
|
|
666
|
+
|
|
667
|
+
result.orphaned = true;
|
|
668
|
+
return result;
|
|
669
|
+
}
|
|
670
|
+
|
|
439
671
|
/**
|
|
440
672
|
* Hard-delete a terminal-status run record and best-effort remove its
|
|
441
673
|
* artifact directory. Result envelope (matches PR #3008 shape so the
|
|
442
674
|
* dashboard DELETE /api/qa/runs/<id> handler stays generic):
|
|
443
675
|
*
|
|
444
|
-
* { ok: true, id, artifactsRemoved: bool }
|
|
445
|
-
* { ok: false, error: 'invalid_id' | 'not_found' | 'not_terminal',
|
|
446
|
-
* currentStatus?: string }
|
|
676
|
+
* { ok: true, id, artifactsRemoved: bool, deletedAs: 'terminal' | 'orphaned-pending' }
|
|
677
|
+
* { ok: false, error: 'invalid_id' | 'not_found' | 'not_terminal' | 'not_orphaned',
|
|
678
|
+
* currentStatus?: string, reason?: string }
|
|
447
679
|
*
|
|
448
680
|
* Caller must cancel/complete the run via the lifecycle helpers before
|
|
449
681
|
* deletion — running/pending runs are refused so an operator can't vacuum a
|
|
450
|
-
* live QA agent out from under itself.
|
|
451
|
-
*
|
|
682
|
+
* live QA agent out from under itself. `allowOrphanedPending` opts into the
|
|
683
|
+
* single narrow exception documented on evaluateOrphanedPendingRun; a pending
|
|
684
|
+
* run that fails ANY of its conditions is still refused, with `not_orphaned`
|
|
685
|
+
* naming the failed condition. The artifact rmSync runs OUTSIDE the lock, with
|
|
686
|
+
* a path.resolve + startsWith() belt-and-braces sandbox.
|
|
452
687
|
*
|
|
453
688
|
* @param {string} id - run id (must pass _isSafeRunId)
|
|
689
|
+
* @param {object} [opts]
|
|
690
|
+
* @param {boolean} [opts.allowOrphanedPending] - allow provably-orphaned pending runs
|
|
454
691
|
* @returns {object} envelope (see above)
|
|
455
692
|
*/
|
|
456
|
-
function deleteQaRun(id) {
|
|
693
|
+
function deleteQaRun(id, { allowOrphanedPending = false } = {}) {
|
|
457
694
|
if (!_isSafeRunId(id)) return { ok: false, error: 'invalid_id' };
|
|
695
|
+
|
|
696
|
+
// Orphan evidence gathering reads QA sessions, work items, runbook files and
|
|
697
|
+
// managed specs — none of that may run inside a mutateQaRuns callback. So
|
|
698
|
+
// evaluate first, then re-assert the cheap in-record invariants inside the
|
|
699
|
+
// transaction: a concurrent markRunning() between the two still wins.
|
|
700
|
+
let orphanEval = null;
|
|
701
|
+
if (allowOrphanedPending) {
|
|
702
|
+
orphanEval = evaluateOrphanedPendingRun(id);
|
|
703
|
+
if (orphanEval.exists && !orphanEval.orphaned && !TERMINAL_STATUSES.has(orphanEval.status)) {
|
|
704
|
+
return {
|
|
705
|
+
ok: false,
|
|
706
|
+
error: 'not_orphaned',
|
|
707
|
+
currentStatus: orphanEval.status,
|
|
708
|
+
reason: orphanEval.reason,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
458
713
|
let captured = null;
|
|
459
714
|
let notFound = false;
|
|
460
715
|
let notTerminal = null;
|
|
716
|
+
let deletedAs = 'terminal';
|
|
461
717
|
mutateQaRuns((runs) => {
|
|
462
718
|
if (!Array.isArray(runs)) return runs;
|
|
463
719
|
const idx = runs.findIndex(r => r && r.id === id);
|
|
464
720
|
if (idx < 0) { notFound = true; return runs; }
|
|
465
721
|
const run = runs[idx];
|
|
466
722
|
if (!TERMINAL_STATUSES.has(run.status)) {
|
|
467
|
-
|
|
468
|
-
|
|
723
|
+
const orphanCleanup = allowOrphanedPending
|
|
724
|
+
&& !!orphanEval && orphanEval.orphaned
|
|
725
|
+
&& run.status === QA_RUN_STATUS.PENDING
|
|
726
|
+
&& !run.startedAt && !run.completedAt && !run.workItemId;
|
|
727
|
+
if (!orphanCleanup) {
|
|
728
|
+
notTerminal = run.status;
|
|
729
|
+
return runs;
|
|
730
|
+
}
|
|
731
|
+
deletedAs = 'orphaned-pending';
|
|
469
732
|
}
|
|
470
733
|
captured = run;
|
|
471
734
|
runs.splice(idx, 1);
|
|
@@ -488,7 +751,7 @@ function deleteQaRun(id) {
|
|
|
488
751
|
} catch (e) {
|
|
489
752
|
log('warn', `qa-runs: rm artifacts dir failed for ${id}: ${e.message}`);
|
|
490
753
|
}
|
|
491
|
-
return { ok: true, id, artifactsRemoved, run: captured };
|
|
754
|
+
return { ok: true, id, artifactsRemoved, deletedAs, run: captured };
|
|
492
755
|
}
|
|
493
756
|
|
|
494
757
|
/**
|
|
@@ -527,16 +790,24 @@ module.exports = {
|
|
|
527
790
|
completeRun,
|
|
528
791
|
setRunWorkItemId,
|
|
529
792
|
deleteQaRun,
|
|
793
|
+
evaluateOrphanedPendingRun,
|
|
794
|
+
QA_ORPHAN_MIN_AGE_MS,
|
|
530
795
|
_isSafeRunId,
|
|
531
796
|
getRun,
|
|
532
797
|
listRuns,
|
|
533
798
|
getRunsForWorkItem,
|
|
534
799
|
summarizeRunsForStatus,
|
|
800
|
+
isPrimaryArtifact,
|
|
801
|
+
selectPrimaryArtifacts,
|
|
802
|
+
QA_PRIMARY_ARTIFACT_DEFAULT_MAX,
|
|
535
803
|
// Exposed for tests:
|
|
536
804
|
validateTransition,
|
|
537
805
|
isValidStatus,
|
|
806
|
+
normalizeArtifact, normalizeJourneyCoverage, // exported for testing
|
|
538
807
|
QA_RUNS_MAX_RECORDS,
|
|
539
808
|
QA_RUN_ID_MAX,
|
|
540
809
|
QA_ARTIFACT_PATH_MAX,
|
|
541
810
|
QA_ARTIFACT_SEGMENT_MAX,
|
|
811
|
+
QA_JOURNEY_COVERAGE_MAX,
|
|
812
|
+
QA_JOURNEY_COVERAGE_STATUSES,
|
|
542
813
|
};
|