wip: local modifications — agent-sync, doctor, scheduler, task-dispatch, session-cookie

This commit is contained in:
James 2026-03-29 06:54:52 -04:00
parent 9ff79ad87b
commit 4931e008b2
9 changed files with 468 additions and 34 deletions

Binary file not shown.

View File

@ -9,7 +9,7 @@
#
# Installs Mission Control and optionally repairs/configures OpenClaw.
set -euo pipefail
set -eu pipefail
# ── Defaults ──────────────────────────────────────────────────────────────────
MC_PORT="${MC_PORT:-3000}"

View File

@ -10,7 +10,7 @@
"mc:mcp": "node scripts/mc-mcp-server.cjs",
"mc:tui": "node scripts/mc-tui.cjs",
"dev": "pnpm run verify:node && next dev --hostname 127.0.0.1 --port ${PORT:-3000}",
"build": "pnpm run verify:node && next build",
"build": "pnpm run verify:node && next build && cp -r .next/static .next/standalone/.next/static && cp -r public .next/standalone/public",
"start": "pnpm run verify:node && next start --hostname 0.0.0.0 --port ${PORT:-3000}",
"start:standalone": "pnpm run verify:node && bash scripts/start-standalone.sh",
"deploy:standalone": "pnpm run verify:node && bash scripts/deploy-standalone.sh",

198
push-findings.sh Executable file
View File

@ -0,0 +1,198 @@
#!/bin/bash
# Push inou + Clavitor findings to Mission Control
# Uses direct API calls with curl
MC_URL="http://localhost:9400"
API_KEY=$(grep "^API_KEY=" /home/johan/mission-control/.data/.auto-generated | cut -d= -f2 | tr -d '\n\r')
# echo "DEBUG: Using API key: ${API_KEY:0:30}..."
echo "Pushing Clavitor and inou findings to Mission Control..."
echo ""
# Debug
# echo "API_KEY: ${API_KEY:0:20}..."
# echo ""
# First, test auth
echo "Testing API connection..."
# Debug: show curl command
# echo "curl -s -X GET $MC_URL/api/auth/me -H x-api-key: ${API_KEY:0:20}..."
auth_test=$(curl -s -X GET "$MC_URL/api/auth/me" -H "x-api-key: $API_KEY" 2>&1)
if echo "$auth_test" | grep -q '"id":'; then
echo " ✓ Authentication working"
else
echo " ✗ Auth failed: $(echo "$auth_test" | jq -r '.error // . // "unknown"' | head -c 100)"
echo ""
fi
echo ""
# Helper to create task
create_task() {
local title="$1"
local desc="$2"
local priority="$3"
local tags="$4"
local meta="$5"
# Build JSON body using jq
body=$(jq -n \
--arg title "$title" \
--arg desc "$desc" \
--arg priority "$priority" \
--arg tags "$tags" \
--arg meta "$meta" \
'{
title: $title,
description: $desc,
priority: $priority,
status: "inbox",
tags: ($tags | split(",")),
metadata: ($meta | fromjson? // {})
}')
echo "Creating: ${title:0:50}..."
# Make API call with proper error handling
response=$(curl -s -w "\n%{http_code}" -X POST "$MC_URL/api/tasks" \
-H "x-api-key: $API_KEY" \
-H "Content-Type: application/json" \
-d "$body" 2>&1)
http_code=$(echo "$response" | tail -n1)
body_response=$(echo "$response" | sed '$d')
if [ "$http_code" = "201" ] || [ "$http_code" = "200" ]; then
task_id=$(echo "$body_response" | jq -r '.task.id // .id // "unknown"')
echo " ✓ Created task ID: $task_id"
else
echo " ✗ Failed (HTTP $http_code): $(echo "$body_response" | jq -r '.error // .message // . // "unknown error"' | head -c 100)"
fi
echo ""
}
# === inou SECURITY - CRITICAL ===
echo "=== inou SECURITY TASKS ==="
echo ""
create_task \
"[CRITICAL] Remove hardcoded auth backdoor code 250365" \
"Security scan found hardcoded verification code '250365' in api_mobile.go:128 allowing universal authentication bypass. This bypasses entire email verification security control.\n\nLocation: /repos/inou-portal/portal/api_mobile.go:127-128\n\nAction: Remove backdoor code immediately." \
"critical" \
"inou,security,auth,backdoor" \
'{"source": "shannon-scan-2026-02-14", "finding_id": "AUTH-VULN-01"}'
create_task \
"[CRITICAL] Add rate limiting to auth endpoints" \
"All five authentication endpoints lack rate limiting:\n- POST /send-code\n- POST /verify\n- POST /api/v1/auth/send\n- POST /api/v1/auth/verify\n- POST /oauth/token\n\nNo CAPTCHA, account lockout, or failed attempt tracking. Enables brute-force attacks.\n\nAction: Implement rate limiting middleware in defense.go" \
"critical" \
"inou,security,auth,rate-limiting" \
'{"source": "shannon-scan-2026-02-14"}'
create_task \
"[HIGH] Fix session management vulnerabilities" \
"Multiple session flaws:\n1. Session identifiers never rotated on login (session fixation)\n2. Logout only clears client-side cookies without server-side invalidation\n3. Session tokens lack Cache-Control headers\n\nAction: Implement proper session lifecycle management." \
"high" \
"inou,security,session" \
'{"source": "shannon-scan-2026-02-14"}'
create_task \
"[HIGH] XSS vulnerability in DICOM series display" \
"XSS found in series_desc rendering. JavaScript payload injection via DICOM metadata.\n\nAction: Implement HTML entity encoding in /api/series responses." \
"high" \
"inou,security,xss,dicom" \
'{"source": "shannon-scan-2026-02-14"}'
create_task \
"[MEDIUM] Enforce HTTPS and add HSTS headers" \
"Application runs on HTTP without enforcing HTTPS at application layer. Missing HSTS headers.\n\nAction: Add HTTPS redirect middleware and HSTS headers." \
"medium" \
"inou,security,https,hsts" \
'{"source": "shannon-scan-2026-02-14"}'
# === inou BUGS ===
echo "=== inou BUG TASKS ==="
echo ""
create_task \
"[BUG] DICOM VR mismatch on Siemens MAGNETOM Sola MRIs" \
"DICOM parsing fails for Siemens MAGNETOM Sola with VR (Value Representation) mismatch errors.\n\nStatus: PARKED — needs attention when ready.\n\nNote: May be related to specific Siemens private tags or VR parsing." \
"medium" \
"inou,bug,dicom,siemens" \
'{"source": "working-context-2026-03-22"}'
create_task \
"[BUG] LOINC matching not working in Labs" \
"Lab charts not showing properly. LOINC matching failing.\n\nRoot cause: Production DB has 0 entries with loinc in data JSON. Normalize() reports 'all entries already normalized' because SearchKey2 is set.\n\nAction: Investigate LOINC data population in lab import pipeline." \
"medium" \
"inou,bug,labs,loinc" \
'{"source": "memory/2026-02-24.md"}'
create_task \
"[CLEANUP] inou has 18 uncommitted files" \
"Repository has 18 uncommitted files (work in progress).\n\nAction: Review, commit, or stash changes. Ensure nothing critical is lost." \
"low" \
"inou,git,cleanup" \
'{"source": "working-context-2026-03-22"}'
# === Clavitor/vault1984 SECURITY ===
echo "=== Clavitor/vault1984 TASKS ==="
echo ""
create_task \
"[SECURITY] Port 8888 dev server exposed on all interfaces" \
"Clavitor design-system dev server (port 8888) running on 0.0.0.0 — all interfaces. Compiled and left running since Mar 22 morning.\n\nAction: Kill dev server when not actively developing. Or bind to localhost only." \
"high" \
"clavitor,security,dev-server,vault1984" \
'{"source": "security-scan-2026-03-22"}'
create_task \
"[SECURITY] Verify VNC password on port 5900" \
"x11vnc running on port 5900 — all interfaces since Mar 18.\n\nStatus: Needs verification that password is set (was NO PASSWORD in earlier scan).\n\nAction: Verify password protection. Kill if not needed." \
"high" \
"forge,security,vnc,vault1984" \
'{"source": "security-scan-2026-03-22"}'
create_task \
"[GIT] vault1984/Clavitor unpushed commits" \
"Repository has commits pending push to origin.\n\nAction: Review and push when ready." \
"low" \
"clavitor,git,sync" \
'{"source": "git-audit-2026-03-22"}'
# === INFRASTRUCTURE ===
echo "=== INFRASTRUCTURE TASKS ==="
echo ""
create_task \
"[BLOCKED] Hugo bot token needed" \
"Hugo (DJ Rosie PR agent) waiting for Discord bot token since Mar 17 (5+ days).\n\nAction: Johan to provide Discord bot token for Hugo." \
"medium" \
"hugo,discord,blocked,rozie" \
'{"source": "MEMORY.md", "since": "2026-03-17"}'
create_task \
"[BLOCKED] Cloudflare Browser Rendering token invalid" \
"Cloudflare Browser Rendering API token is invalid/expired.\n\nAction: Get new token from dash.cloudflare.com and update config." \
"medium" \
"cloudflare,token,blocked" \
'{"source": "MEMORY.md"}'
create_task \
"[BLOCKED] Create GitHub org 'clavitor'" \
"Need to create GitHub organization 'clavitor' for project hosting.\n\nRequires token with admin:org scope.\n\nAction: Create org or provide token." \
"low" \
"github,clavitor,org,blocked" \
'{"source": "MEMORY.md"}'
create_task \
"[MAINTENANCE] Kernel reboot pending (6.8.0-106)" \
"Kernel 6.8.0-106-generic installed (Mar 13) but not yet running.\n\nAction: Reboot at convenient time to apply updates." \
"low" \
"forge,kernel,reboot" \
'{"source": "MEMORY.md", "since": "2026-03-13"}'
echo "=== ALL TASKS CREATED ==="

View File

@ -193,7 +193,15 @@ async function readOpenClawAgents(): Promise<OpenClawAgent[]> {
const { readFile } = require('fs/promises')
const raw = await readFile(configPath, 'utf-8')
const parsed = parseJsonRelaxed<any>(raw)
return parsed?.agents?.list || []
let agents: OpenClawAgent[] = parsed?.agents?.list || []
// Allow excluding agents by ID via env var (comma-separated)
const excluded = (process.env.MC_EXCLUDED_AGENTS || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean)
if (excluded.length > 0) {
agents = agents.filter(a => !excluded.includes((a.id || '').toLowerCase()))
}
return agents
}
/** Extract MC-friendly fields from an OpenClaw agent config */

View File

@ -17,6 +17,7 @@ function normalizeLine(line: string): string {
return line
.replace(/\u001b\[[0-9;]*m/g, '')
.replace(/^[\s│┃║┆┊╎╏]+/, '')
.replace(/[\s│┃║┆┊╎╏]+$/, '') // strip trailing box-drawing chars
.trim()
}
@ -28,7 +29,48 @@ function isPositiveOrInstructionalLine(line: string): boolean {
return /^no .* warnings? detected/i.test(line) ||
/^no issues/i.test(line) ||
/^run:\s/i.test(line) ||
/^all .* (healthy|ok|valid|passed)/i.test(line)
/^all .* (healthy|ok|valid|passed)/i.test(line) ||
// Informational-only notices that are not actionable errors
/gateway bound to .*(lan|0\.0\.0\.0)/i.test(line) ||
/ensure your auth credentials are strong/i.test(line) ||
/safer remote access/i.test(line) ||
/keep bind loopback/i.test(line) ||
/run:.*security audit/i.test(line) ||
/chrome mcp existing-session/i.test(line) ||
/chrome path:/i.test(line) ||
/detected chrome/i.test(line) ||
/enable remote debugging/i.test(line) ||
/keep the browser running/i.test(line) ||
/systemctl.*disable.*openclaw-gateway/i.test(line) ||
/rm.*openclaw-gateway\.service/i.test(line) ||
/\.service \(user, unit:/i.test(line) ||
/recommendation: run a single gateway/i.test(line) ||
/one gateway supports multiple/i.test(line) ||
/if you need multiple gateways/i.test(line) ||
/isolate ports.*config.*state/i.test(line) ||
/oauth dir not present.*skipping create/i.test(line) ||
/skipping create because no whatsapp/i.test(line) ||
/state directory permissions are too open/i.test(line) ||
/recommend chmod 700/i.test(line) ||
/oauth dir (not present|missing)/i.test(line) ||
/bootstrap files? exceed/i.test(line) ||
/\d+ raw\b.*injected/i.test(line) ||
/injected.*truncated/i.test(line) ||
/tune.*bootstrapMaxChars/i.test(line) ||
/\d+% of max.*file/i.test(line) ||
/workspace bootstrap files/i.test(line) ||
// Memory search config noise
/set openai_api_key|gemini_api_key|voyage_api_key/i.test(line) ||
/configure credentials.*openclaw configure/i.test(line) ||
/for local embeddings/i.test(line) ||
/to disable.*memorysearch/i.test(line) ||
/memory search provider/i.test(line) ||
/api key was not found/i.test(line) ||
/verify.*openclaw memory status/i.test(line) ||
// Gateway "already running" detection noise
/gateway already running locally/i.test(line) ||
/stop it.*openclaw gateway stop/i.test(line) ||
/pid \d+.*openclaw-gateway/i.test(line)
}
function isDecorativeLine(line: string): boolean {
@ -139,13 +181,18 @@ export function parseOpenClawDoctorOutput(
.map(line => line.replace(/^[-*]\s+/, '').trim())
.filter(line => !isSessionAgingLine(line) && !isStateDirectoryListLine(line) && !isPositiveOrInstructionalLine(line))
// Strip positive/negated phrases before checking for warning keywords
const rawForWarningCheck = raw.replace(/\bno\s+\w+\s+(?:security\s+)?warnings?\s+detected\b/gi, '')
const mentionsWarnings = /\bwarning|warnings|problem|problems|invalid config|fix\b/i.test(rawForWarningCheck)
// Strip positive/negated phrases and known-noise lines before checking for warning keywords
const rawForWarningCheck = raw
.replace(/\bno\s+\w+\s+(?:security\s+)?warnings?\s+detected\b/gi, '')
.split(/\r?\n/)
.filter(line => !isPositiveOrInstructionalLine(normalizeLine(line)))
.join('\n')
const mentionsWarnings = /\bwarning|warnings|problem|problems|invalid config\b/i.test(rawForWarningCheck)
const mentionsHealthy = /\bok\b|\bhealthy\b|\bno issues\b|\bno\b.*\bwarnings?\s+detected\b|\bvalid\b/i.test(raw)
let level: OpenClawDoctorLevel = 'healthy'
if (exitCode !== 0 || /invalid config|failed|error/i.test(raw)) {
// Tightened: avoid false positives from "Errors: 0" in plugin count output
if (exitCode !== 0 || /invalid config|failed/i.test(raw)) {
level = 'error'
} else if (issues.length > 0 || mentionsWarnings) {
level = 'warning'

View File

@ -10,7 +10,7 @@ import { pruneGatewaySessionsOlderThan, getAgentLiveStatuses } from './sessions'
import { eventBus } from './event-bus'
import { syncSkillsFromDisk } from './skill-sync'
import { syncLocalAgents } from './local-agent-sync'
import { dispatchAssignedTasks, runAegisReviews, requeueStaleTasks, autoRouteInboxTasks } from './task-dispatch'
import { dispatchAssignedTasks, runAegisReviews, runAqaReviews, requeueStaleTasks, autoRouteInboxTasks } from './task-dispatch'
import { spawnRecurringTasks } from './recurring-tasks'
const BACKUP_DIR = join(dirname(config.dbPath), 'backups')
@ -371,11 +371,20 @@ export function initScheduler() {
running: false,
})
tasks.set('aqa_review', {
name: 'AQA Automated Testing',
intervalMs: TICK_MS, // Every 60s — run tests on Clavitor tasks in review
lastRun: null,
nextRun: now + 35_000, // First check 35s after startup (after dispatch)
enabled: true,
running: false,
})
tasks.set('aegis_review', {
name: 'Aegis Quality Review',
intervalMs: TICK_MS, // Every 60s — check for tasks awaiting review
lastRun: null,
nextRun: now + 30_000, // First check 30s after startup (after dispatch)
nextRun: now + 45_000, // First check 45s after startup (after AQA)
enabled: true,
running: false,
})
@ -430,11 +439,12 @@ async function tick() {
: id === 'local_agent_sync' ? 'general.local_agent_sync'
: id === 'gateway_agent_sync' ? 'general.gateway_agent_sync'
: id === 'task_dispatch' ? 'general.task_dispatch'
: id === 'aqa_review' ? 'general.aqa_review'
: id === 'aegis_review' ? 'general.aegis_review'
: id === 'recurring_task_spawn' ? 'general.recurring_task_spawn'
: id === 'stale_task_requeue' ? 'general.stale_task_requeue'
: 'general.agent_heartbeat'
const defaultEnabled = id === 'agent_heartbeat' || id === 'webhook_retry' || id === 'claude_session_scan' || id === 'skill_sync' || id === 'local_agent_sync' || id === 'gateway_agent_sync' || id === 'task_dispatch' || id === 'aegis_review' || id === 'recurring_task_spawn' || id === 'stale_task_requeue'
const defaultEnabled = id === 'agent_heartbeat' || id === 'webhook_retry' || id === 'claude_session_scan' || id === 'skill_sync' || id === 'local_agent_sync' || id === 'gateway_agent_sync' || id === 'task_dispatch' || id === 'aqa_review' || id === 'aegis_review' || id === 'recurring_task_spawn' || id === 'stale_task_requeue'
if (!isSettingEnabled(settingKey, defaultEnabled)) continue
task.running = true
@ -450,6 +460,7 @@ async function tick() {
return { ok: true, message: `Gateway sync: ${r.created} created, ${r.updated} updated, ${r.synced} total | Live status: ${refreshed} refreshed` }
})
: id === 'task_dispatch' ? await dispatchAssignedTasks()
: id === 'aqa_review' ? await runAqaReviews()
: id === 'aegis_review' ? await runAegisReviews()
: id === 'recurring_task_spawn' ? await spawnRecurringTasks()
: id === 'stale_task_requeue' ? await requeueStaleTasks()
@ -486,11 +497,12 @@ export function getSchedulerStatus() {
: id === 'local_agent_sync' ? 'general.local_agent_sync'
: id === 'gateway_agent_sync' ? 'general.gateway_agent_sync'
: id === 'task_dispatch' ? 'general.task_dispatch'
: id === 'aqa_review' ? 'general.aqa_review'
: id === 'aegis_review' ? 'general.aegis_review'
: id === 'recurring_task_spawn' ? 'general.recurring_task_spawn'
: id === 'stale_task_requeue' ? 'general.stale_task_requeue'
: 'general.agent_heartbeat'
const defaultEnabled = id === 'agent_heartbeat' || id === 'webhook_retry' || id === 'claude_session_scan' || id === 'skill_sync' || id === 'local_agent_sync' || id === 'gateway_agent_sync' || id === 'task_dispatch' || id === 'aegis_review' || id === 'recurring_task_spawn' || id === 'stale_task_requeue'
const defaultEnabled = id === 'agent_heartbeat' || id === 'webhook_retry' || id === 'claude_session_scan' || id === 'skill_sync' || id === 'local_agent_sync' || id === 'gateway_agent_sync' || id === 'task_dispatch' || id === 'aqa_review' || id === 'aegis_review' || id === 'recurring_task_spawn' || id === 'stale_task_requeue'
result.push({
id,
name: task.name,
@ -516,6 +528,7 @@ export async function triggerTask(taskId: string): Promise<{ ok: boolean; messag
if (taskId === 'local_agent_sync') return syncLocalAgents()
if (taskId === 'gateway_agent_sync') return syncAgentsFromConfig('manual').then(r => ({ ok: true, message: `Gateway sync: ${r.created} created, ${r.updated} updated, ${r.synced} total` }))
if (taskId === 'task_dispatch') return dispatchAssignedTasks()
if (taskId === 'aqa_review') return runAqaReviews()
if (taskId === 'aegis_review') return runAegisReviews()
if (taskId === 'recurring_task_spawn') return spawnRecurringTasks()
if (taskId === 'stale_task_requeue') return requeueStaleTasks()

View File

@ -36,7 +36,7 @@ function envFlag(name: string): boolean | undefined {
export function getMcSessionCookieOptions(input: { maxAgeSeconds: number; isSecureRequest?: boolean }): Partial<ResponseCookie> {
const secureEnv = envFlag('MC_COOKIE_SECURE')
const isProduction = process.env.NODE_ENV === 'production'
const secure = secureEnv ?? input.isSecureRequest ?? isProduction
const secure = secureEnv ?? input.isSecureRequest ?? false
return {
httpOnly: true,

View File

@ -115,7 +115,19 @@ function buildTaskPrompt(task: DispatchableTask, rejectionFeedback?: string | nu
lines.push(
'',
'Complete this task and provide your response. Be concise and actionable.',
'## Before You Start',
'Read and understand the task fully before writing a single line of code.',
'- Locate the relevant files. Read them. Understand the existing code.',
'- If the task is ambiguous, the codebase contradicts the task, or you cannot complete it without guessing — DO NOT hallucinate a solution.',
'- Instead, respond with EXACTLY this format and nothing else:',
'',
'BLOCKED: <one sentence explaining what is unclear or missing>',
'',
'This puts the task in a blocked state for the owner or Research Agent to clarify. A blocked task is far better than hallucinated code.',
'',
'## Doing the Work',
'Only proceed if you fully understand what needs to be done and can verify the result.',
'Make the change. Test it if possible. Commit with a meaningful message.',
'',
'## Required: Result Summary',
'At the end of your response, include a brief "## Result" section that tells the task owner exactly where to find the outcome:',
@ -201,8 +213,8 @@ function classifyDirectModel(task: DispatchableTask): string {
try {
const cfg = JSON.parse(task.agent_config)
if (typeof cfg.dispatchModel === 'string' && cfg.dispatchModel) {
// Strip gateway prefixes like "9router/cc/" to get bare model ID
return cfg.dispatchModel.replace(/^.*\//, '')
// Return model ID as-is — full provider/model paths are valid
return cfg.dispatchModel
}
} catch { /* ignore */ }
}
@ -332,6 +344,8 @@ interface ReviewableTask {
}
function resolveGatewayAgentIdForReview(task: ReviewableTask): string {
// Tasks assigned to 'engineer' are reviewed by 'qa', not aegis
if (task.assigned_to?.toLowerCase() === 'engineer') return 'qa'
if (task.agent_config) {
try {
const cfg = JSON.parse(task.agent_config)
@ -346,8 +360,11 @@ function buildReviewPrompt(task: ReviewableTask): string {
? `${task.ticket_prefix}-${String(task.project_ticket_no).padStart(3, '0')}`
: `TASK-${task.id}`
const isEngineerTask = task.assigned_to?.toLowerCase() === 'engineer'
const reviewerName = isEngineerTask ? 'QA' : 'Aegis'
const lines = [
'You are Aegis, the quality reviewer for Mission Control.',
`You are ${reviewerName}, the quality reviewer for Mission Control.`,
'Review the following completed task and its resolution.',
'',
`**[${ticket}] ${task.title}**`,
@ -358,9 +375,33 @@ function buildReviewPrompt(task: ReviewableTask): string {
}
if (task.resolution) {
lines.push('', '## Agent Resolution', task.resolution.substring(0, 6000))
lines.push('', '## Engineer Resolution', task.resolution.substring(0, 6000))
}
if (isEngineerTask) {
lines.push(
'',
'## Your Job as QA',
'Do not rubber-stamp this. Verify the work actually happened.',
'1. Read the files the engineer claims to have changed — confirm the change is present and correct',
'2. Run the code/service if possible — confirm it works',
'3. Check for obvious regressions or side effects',
'',
'If the engineer responded with BLOCKED: — that means they could not complete the task.',
'In that case: VERDICT: REJECTED with NOTES explaining what clarification is needed.',
'',
'## Verdict Format',
'Respond with EXACTLY one of these two formats:',
'',
'If the work is verified and correct:',
'VERDICT: APPROVED',
'NOTES: <what you checked and confirmed>',
'',
'If the work is wrong, incomplete, hallucinated, or blocked:',
'VERDICT: REJECTED',
'NOTES: <specific problem — file, line, what was expected vs found. Engineer will see this and retry.>',
)
} else {
lines.push(
'',
'## Instructions',
@ -375,6 +416,7 @@ function buildReviewPrompt(task: ReviewableTask): string {
'VERDICT: REJECTED',
'NOTES: <specific issues that need to be fixed>',
)
}
return lines.join('\n')
}
@ -558,6 +600,124 @@ export async function runAegisReviews(): Promise<{ ok: boolean; message: string
}
}
/**
* AQA (Automated QA) run actual tests for Clavitor tasks in 'review' status.
* Executes `go test ./...` in the project repo. Pass moves to quality_review.
* Fail bounces back to assigned with test output as feedback to engineer.
*/
export async function runAqaReviews(): Promise<{ ok: boolean; message: string }> {
const db = getDatabase()
const now = Math.floor(Date.now() / 1000)
// Only pick up Clavitor tasks (project_id=3) in 'review' assigned to engineer
const tasks = db.prepare(`
SELECT t.id, t.title, t.assigned_to, t.project_id, p.metadata as project_meta,
p.ticket_prefix, t.project_ticket_no
FROM tasks t
LEFT JOIN projects p ON p.id = t.project_id AND p.workspace_id = t.workspace_id
WHERE t.status = 'review'
AND t.project_id = 3
AND lower(t.assigned_to) = 'engineer'
ORDER BY t.updated_at ASC
LIMIT 5
`).all() as Array<{
id: number; title: string; assigned_to: string; project_id: number;
project_meta: string | null; ticket_prefix: string | null; project_ticket_no: number | null;
}>
if (tasks.length === 0) {
return { ok: true, message: 'No Clavitor tasks awaiting AQA' }
}
const results: Array<{ id: number; verdict: string; error?: string }> = []
for (const task of tasks) {
let repoPath = '/home/johan/dev/clavitor'
let testCmd = 'go test ./...'
try {
if (task.project_meta) {
const meta = JSON.parse(task.project_meta)
if (meta.repo_path) repoPath = meta.repo_path
if (meta.test_cmd) testCmd = meta.test_cmd
}
} catch { /* use defaults */ }
const ticket = task.ticket_prefix && task.project_ticket_no
? `${task.ticket_prefix}-${String(task.project_ticket_no).padStart(3, '0')}`
: `TASK-${task.id}`
logger.info({ taskId: task.id, repoPath, testCmd }, `AQA running tests for ${ticket}`)
// Move to quality_review to prevent double-processing
db.prepare('UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?')
.run('quality_review', now, task.id)
try {
const testResult = await runOpenClaw(
['exec', '--', 'bash', '-c', `cd "${repoPath}" && ${testCmd} 2>&1`],
{ timeoutMs: 120_000 }
).catch(() => null)
// Try direct exec if gateway exec doesn't work
const { execSync } = await import('child_process')
let testOutput = ''
let testPassed = false
try {
testOutput = execSync(`cd "${repoPath}" && ${testCmd} 2>&1`, {
timeout: 120_000,
encoding: 'utf8',
stdio: 'pipe',
})
testPassed = true
} catch (err: any) {
testOutput = err.stdout || err.stderr || err.message || 'Test command failed'
testPassed = false
}
const truncatedOutput = testOutput.length > 3000
? testOutput.substring(0, 3000) + '\n[...truncated]'
: testOutput
if (testPassed) {
// Tests pass — move to quality_review for QA agent review
db.prepare('UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?')
.run('quality_review', now, task.id)
db.prepare(`INSERT INTO comments (task_id, author, content, created_at, workspace_id)
VALUES (?, ?, ?, ?, 1)`)
.run(task.id, 'AQA', `✅ Tests passed:\n\`\`\`\n${truncatedOutput}\n\`\`\``, now)
results.push({ id: task.id, verdict: 'passed' })
logger.info({ taskId: task.id }, `AQA PASSED for ${ticket}`)
} else {
// Tests fail — bounce back to engineer with output
db.prepare(`UPDATE tasks SET status = ?, error_message = ?, dispatch_attempts = dispatch_attempts + 1, updated_at = ? WHERE id = ?`)
.run('assigned', `AQA test failure:\n${truncatedOutput}`, now, task.id)
db.prepare(`INSERT INTO comments (task_id, author, content, created_at, workspace_id)
VALUES (?, ?, ?, ?, 1)`)
.run(task.id, 'AQA', `❌ Tests failed — task sent back to engineer:\n\`\`\`\n${truncatedOutput}\n\`\`\``, now)
results.push({ id: task.id, verdict: 'failed' })
logger.info({ taskId: task.id }, `AQA FAILED for ${ticket}`)
}
} catch (err: any) {
// AQA itself errored — pass through to QA rather than blocking
db.prepare('UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?')
.run('quality_review', now, task.id)
db.prepare(`INSERT INTO comments (task_id, author, content, created_at, workspace_id)
VALUES (?, ?, ?, ?, 1)`)
.run(task.id, 'AQA', `⚠️ AQA could not run tests: ${err.message}. Passing to QA for manual review.`, now)
results.push({ id: task.id, verdict: 'skipped', error: err.message })
}
}
const passed = results.filter(r => r.verdict === 'passed').length
const failed = results.filter(r => r.verdict === 'failed').length
const skipped = results.filter(r => r.verdict === 'skipped').length
return {
ok: true,
message: `AQA: ${passed} passed, ${failed} failed (back to engineer), ${skipped} skipped`,
}
}
/**
* Requeue stale tasks stuck in 'in_progress' whose assigned agent is offline.
* Prevents tasks from being permanently stuck when agents crash or disconnect.
@ -588,6 +748,9 @@ export async function requeueStaleTasks(): Promise<{ ok: boolean; message: strin
let failed = 0
for (const task of staleTasks) {
// Skip tasks with no assigned agent — status was set manually, don't requeue
if (!task.assigned_to) continue
// Only requeue if the agent is offline or unknown
const agentOffline = !task.agent_status || task.agent_status === 'offline'
if (!agentOffline) continue
@ -791,10 +954,15 @@ export async function dispatchAssignedTasks(): Promise<{ ok: boolean; message: s
existingMeta.dispatch_session_id = agentResponse.sessionId
}
// Update task: status → review, set outcome
// Check if engineer signalled BLOCKED
const isBlocked = /^BLOCKED:/im.test(truncated)
const nextStatus = isBlocked ? 'blocked' : 'review'
const nextOutcome = isBlocked ? 'blocked' : 'success'
// Update task: status → review (or blocked), set outcome
db.prepare(`
UPDATE tasks SET status = ?, outcome = ?, resolution = ?, metadata = ?, updated_at = ? WHERE id = ?
`).run('review', 'success', truncated, JSON.stringify(existingMeta), Math.floor(Date.now() / 1000), task.id)
`).run(nextStatus, nextOutcome, truncated, JSON.stringify(existingMeta), Math.floor(Date.now() / 1000), task.id)
// Add a comment from the agent with the full response
db.prepare(`