patchwarden 1.6.1 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +17 -12
- package/README.md +14 -11
- package/dist/assessments/assessmentDiagnostics.d.ts +29 -0
- package/dist/assessments/assessmentDiagnostics.js +132 -0
- package/dist/assessments/assessmentStore.d.ts +39 -0
- package/dist/assessments/assessmentStore.js +166 -12
- package/dist/assessments/securitySnapshot.d.ts +50 -0
- package/dist/assessments/securitySnapshot.js +158 -0
- package/dist/control/routes/status.d.ts +14 -0
- package/dist/control/routes/status.js +22 -0
- package/dist/control/runtime.js +2 -10
- package/dist/control/server.js +1 -0
- package/dist/diagnostics/allowedTestCommandSafety.d.ts +1 -0
- package/dist/diagnostics/allowedTestCommandSafety.js +11 -0
- package/dist/direct/directSessionStore.js +5 -4
- package/dist/doctor.js +3 -5
- package/dist/runner/runTask.d.ts +1 -0
- package/dist/runner/runTask.js +47 -6
- package/dist/runner/taskRuntime.d.ts +2 -0
- package/dist/runner/taskStatusStore.js +12 -1
- package/dist/runner/watch.js +64 -1
- package/dist/smoke-test.js +3 -3
- package/dist/tools/definitions/toolDefs.js +3 -3
- package/dist/tools/diagnostics/auditTask.d.ts +1 -0
- package/dist/tools/diagnostics/auditTask.js +35 -7
- package/dist/tools/diagnostics/healthCheck.d.ts +12 -0
- package/dist/tools/diagnostics/healthCheck.js +29 -1
- package/dist/tools/tasks/cancelTask.js +2 -1
- package/dist/tools/tasks/createTask.d.ts +2 -2
- package/dist/tools/tasks/createTask.js +70 -9
- package/dist/tools/tasks/diagnoseTask.js +4 -8
- package/dist/tools/tasks/getTaskStatus.js +6 -1
- package/dist/tools/tasks/getTaskSummary.js +2 -10
- package/dist/tools/tasks/listTasks.js +2 -1
- package/dist/tools/tasks/reconcileTasks.d.ts +2 -1
- package/dist/tools/tasks/reconcileTasks.js +185 -39
- package/dist/tools/tasks/runTaskLoop.js +5 -11
- package/dist/tools/tasks/taskOutputs.d.ts +2 -2
- package/dist/tools/tasks/taskStates.d.ts +6 -0
- package/dist/tools/tasks/taskStates.js +52 -0
- package/dist/tools/tasks/waitForTask.js +3 -11
- package/dist/tools/workspace/listAgents.d.ts +6 -0
- package/dist/tools/workspace/listAgents.js +12 -2
- package/dist/utils/atomicFile.js +20 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/CODE_WIKI.md +4 -4
- package/docs/open-source-application.md +11 -12
- package/docs/release-evidence.md +33 -47
- package/package.json +1 -1
- package/scripts/checks/control-center-smoke.js +11 -4
- package/scripts/checks/lifecycle-smoke.js +5 -2
- package/scripts/checks/mcp-smoke.js +31 -1
- package/scripts/checks/watcher-supervisor-smoke.js +29 -1
- package/scripts/control/start-patchwarden-tunnel.ps1 +65 -1
- package/scripts/e2e/demo-runtime.mjs +153 -0
- package/scripts/release/desktop-preflight.js +1 -1
- package/src/assessments/assessmentDiagnostics.ts +172 -0
- package/src/assessments/assessmentStore.ts +237 -12
- package/src/assessments/securitySnapshot.ts +231 -0
- package/src/control/routes/status.ts +37 -0
- package/src/control/runtime.ts +2 -10
- package/src/control/server.ts +1 -0
- package/src/diagnostics/allowedTestCommandSafety.ts +12 -0
- package/src/direct/directSessionStore.ts +6 -4
- package/src/doctor.ts +3 -5
- package/src/runner/runTask.ts +59 -6
- package/src/runner/taskRuntime.ts +2 -0
- package/src/runner/taskStatusStore.ts +16 -1
- package/src/runner/watch.ts +67 -1
- package/src/smoke-test.ts +2 -2
- package/src/tools/definitions/toolDefs.ts +3 -3
- package/src/tools/diagnostics/auditTask.ts +37 -7
- package/src/tools/diagnostics/healthCheck.ts +29 -1
- package/src/tools/tasks/cancelTask.ts +2 -1
- package/src/tools/tasks/createTask.ts +113 -8
- package/src/tools/tasks/diagnoseTask.ts +4 -8
- package/src/tools/tasks/getTaskStatus.ts +6 -1
- package/src/tools/tasks/getTaskSummary.ts +2 -11
- package/src/tools/tasks/listTasks.ts +2 -1
- package/src/tools/tasks/reconcileTasks.ts +162 -13
- package/src/tools/tasks/runTaskLoop.ts +4 -12
- package/src/tools/tasks/taskStates.ts +54 -0
- package/src/tools/tasks/waitForTask.ts +3 -12
- package/src/tools/workspace/listAgents.ts +17 -3
- package/src/utils/atomicFile.ts +17 -1
- package/src/version.ts +1 -1
- package/ui/pages/dashboard.html +4 -0
- package/ui/pages/logs.html +29 -0
- package/ui/pages/settings.html +3 -3
- package/ui/settings.js +26 -17
package/src/runner/watch.ts
CHANGED
|
@@ -38,6 +38,11 @@ import { mutateTaskStatus } from "./taskStatusStore.js";
|
|
|
38
38
|
import { logger } from "../logging.js";
|
|
39
39
|
import { redactSensitiveContent } from "../security/contentRedaction.js";
|
|
40
40
|
import { atomicWriteFileSync } from "../utils/atomicFile.js";
|
|
41
|
+
import { reconcileTasks } from "../tools/tasks/reconcileTasks.js";
|
|
42
|
+
import {
|
|
43
|
+
buildAssessmentValidationFailureDiagnostic,
|
|
44
|
+
recordAssessmentValidationFailure,
|
|
45
|
+
} from "../assessments/assessmentDiagnostics.js";
|
|
41
46
|
// Note: `runTask` is imported lazily inside `tick()` via `await import("./runTask.js")`.
|
|
42
47
|
// This keeps `import { acquireWatcherLock }` side-effect-free for unit tests and
|
|
43
48
|
// avoids eagerly pulling in the full tools/dispatch graph at module load.
|
|
@@ -60,6 +65,11 @@ const WATCHER_LAUNCHER_PID = Number.isInteger(Number(process.env.PATCHWARDEN_WAT
|
|
|
60
65
|
// Track executed tasks to prevent re-execution
|
|
61
66
|
const executedTasks = new Set<string>();
|
|
62
67
|
let consecutiveTickFailures = 0;
|
|
68
|
+
let activeTaskId: string | null = null;
|
|
69
|
+
let activeTaskPhase: string | null = null;
|
|
70
|
+
let tickRunning = false;
|
|
71
|
+
let tickStartedAt: string | null = null;
|
|
72
|
+
let lastTickCompletedAt: string | null = null;
|
|
63
73
|
|
|
64
74
|
// ── Single-instance lock ─────────────────────────────────────────
|
|
65
75
|
// watch.ts is a long-running poller; without a lock, two watchers started
|
|
@@ -212,6 +222,8 @@ export function createNonOverlappingRunner(
|
|
|
212
222
|
// ── Main loop ─────────────────────────────────────────────────────
|
|
213
223
|
|
|
214
224
|
async function tick() {
|
|
225
|
+
tickRunning = true;
|
|
226
|
+
tickStartedAt = new Date().toISOString();
|
|
215
227
|
try {
|
|
216
228
|
writeWatcherHeartbeat("running");
|
|
217
229
|
// Ensure tasks directory exists
|
|
@@ -221,6 +233,8 @@ async function tick() {
|
|
|
221
233
|
return;
|
|
222
234
|
}
|
|
223
235
|
|
|
236
|
+
reconcileInterruptedTasks();
|
|
237
|
+
|
|
224
238
|
const entries = readdirSync(tasksDir, { withFileTypes: true });
|
|
225
239
|
const taskDirs = entries.filter((e) => e.isDirectory());
|
|
226
240
|
|
|
@@ -280,7 +294,9 @@ async function tick() {
|
|
|
280
294
|
const preExecSnapshot = await captureRepoSnapshot(resolvedRepoPath);
|
|
281
295
|
const validation = validateAssessmentFreshness(statusData.assessment_id, preExecSnapshot);
|
|
282
296
|
if (!validation.valid) {
|
|
283
|
-
|
|
297
|
+
recordAssessmentValidationFailure(validation);
|
|
298
|
+
const diagnostic = buildAssessmentValidationFailureDiagnostic(validation);
|
|
299
|
+
throw new Error(`assessment validation failed: ${JSON.stringify(diagnostic)}`);
|
|
284
300
|
}
|
|
285
301
|
}
|
|
286
302
|
} catch (err) {
|
|
@@ -312,13 +328,20 @@ async function tick() {
|
|
|
312
328
|
// ── Execute ──
|
|
313
329
|
logger.info(`[watcher] Executing: ${taskId}`);
|
|
314
330
|
executedTasks.add(taskId);
|
|
331
|
+
activeTaskId = taskId;
|
|
332
|
+
activeTaskPhase = "executing_agent";
|
|
315
333
|
|
|
316
334
|
try {
|
|
317
335
|
const { runTask } = await import("./runTask.js");
|
|
318
336
|
const result = await runTask(taskId);
|
|
337
|
+
activeTaskPhase = result.status;
|
|
319
338
|
logger.info(`[watcher] ${taskId} → ${result.status}`);
|
|
320
339
|
} catch (err) {
|
|
340
|
+
activeTaskPhase = "error";
|
|
321
341
|
logger.error(`[watcher] ${taskId} → error: ${err instanceof Error ? err.message : String(err)}`);
|
|
342
|
+
} finally {
|
|
343
|
+
activeTaskId = null;
|
|
344
|
+
activeTaskPhase = null;
|
|
322
345
|
}
|
|
323
346
|
}
|
|
324
347
|
consecutiveTickFailures = 0;
|
|
@@ -334,6 +357,26 @@ async function tick() {
|
|
|
334
357
|
error: heartbeatError instanceof Error ? heartbeatError.message : String(heartbeatError),
|
|
335
358
|
});
|
|
336
359
|
}
|
|
360
|
+
} finally {
|
|
361
|
+
tickRunning = false;
|
|
362
|
+
lastTickCompletedAt = new Date().toISOString();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function reconcileInterruptedTasks(): void {
|
|
367
|
+
const result = reconcileTasks({
|
|
368
|
+
mode: "safe_fix",
|
|
369
|
+
max_age_minutes: Math.max(config.watcherStaleSeconds / 60, 1 / 60),
|
|
370
|
+
include_done_candidates: true,
|
|
371
|
+
}, config);
|
|
372
|
+
if (result.reconciled > 0) {
|
|
373
|
+
logger.warn("[watcher] Reconciled tasks left by a previous runner", {
|
|
374
|
+
reconciled: result.reconciled,
|
|
375
|
+
task_ids: result.reports
|
|
376
|
+
.filter((report) => report.action_taken !== "left_unchanged")
|
|
377
|
+
.map((report) => report.task_id)
|
|
378
|
+
.slice(0, 20),
|
|
379
|
+
});
|
|
337
380
|
}
|
|
338
381
|
}
|
|
339
382
|
|
|
@@ -347,9 +390,30 @@ function writeWatcherHeartbeat(status: "running" | "degraded", lastError?: strin
|
|
|
347
390
|
launcher_pid: WATCHER_LAUNCHER_PID,
|
|
348
391
|
consecutive_failures: consecutiveTickFailures,
|
|
349
392
|
last_error: lastError ? lastError.slice(0, 500) : null,
|
|
393
|
+
heartbeat_source: "independent_interval",
|
|
394
|
+
tick_running: tickRunning,
|
|
395
|
+
tick_started_at: tickStartedAt,
|
|
396
|
+
last_tick_completed_at: lastTickCompletedAt,
|
|
397
|
+
active_task_id: activeTaskId,
|
|
398
|
+
active_task_phase: activeTaskPhase,
|
|
350
399
|
}, null, 2));
|
|
351
400
|
}
|
|
352
401
|
|
|
402
|
+
function startWatcherHeartbeatLoop(): ReturnType<typeof setInterval> {
|
|
403
|
+
const intervalMs = Math.min(2000, Math.max(1000, Math.floor(config.watcherStaleSeconds * 1000 / 3)));
|
|
404
|
+
const timer = setInterval(() => {
|
|
405
|
+
try {
|
|
406
|
+
writeWatcherHeartbeat("running");
|
|
407
|
+
} catch (error) {
|
|
408
|
+
logger.warn("[watcher] Independent heartbeat write failed", {
|
|
409
|
+
error: error instanceof Error ? error.message : String(error),
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}, intervalMs);
|
|
413
|
+
timer.unref?.();
|
|
414
|
+
return timer;
|
|
415
|
+
}
|
|
416
|
+
|
|
353
417
|
// ── Start (only when executed as a script) ─────────────────────────
|
|
354
418
|
// The bootstrap below has process-wide side effects (mkdir, signal handlers,
|
|
355
419
|
// polling interval, lock acquisition). Guarding it with a main-module check
|
|
@@ -378,6 +442,7 @@ if (isMainModule) {
|
|
|
378
442
|
}
|
|
379
443
|
|
|
380
444
|
logger.info("[watcher] Started");
|
|
445
|
+
const heartbeatTimer = startWatcherHeartbeatLoop();
|
|
381
446
|
const scheduledTick = createNonOverlappingRunner(tick, () => {
|
|
382
447
|
logger.warn("[watcher] Previous tick is still running; skipping overlapping poll");
|
|
383
448
|
});
|
|
@@ -389,6 +454,7 @@ if (isMainModule) {
|
|
|
389
454
|
// Graceful shutdown
|
|
390
455
|
function shutdown(): void {
|
|
391
456
|
logger.info("[watcher] Stopped");
|
|
457
|
+
clearInterval(heartbeatTimer);
|
|
392
458
|
try { releaseWatcherLock(WATCHER_LOCK_FILE); } catch (error) {
|
|
393
459
|
logger.warn("[watcher] Failed to release watcher lock", {
|
|
394
460
|
error: error instanceof Error ? error.message : String(error),
|
package/src/smoke-test.ts
CHANGED
|
@@ -1338,7 +1338,7 @@ await test("K8. assessment not found rejected", async () => {
|
|
|
1338
1338
|
agent: "codex",
|
|
1339
1339
|
repo_path: ".",
|
|
1340
1340
|
execution_mode: "execute",
|
|
1341
|
-
assessment_id: "
|
|
1341
|
+
assessment_id: "assessment_20260101_000000_" + "0".repeat(32),
|
|
1342
1342
|
});
|
|
1343
1343
|
throw new Error("Should have rejected unknown assessment");
|
|
1344
1344
|
} catch (e: any) {
|
|
@@ -1569,7 +1569,7 @@ await test("K16. assessment_short_id cannot execute", async () => {
|
|
|
1569
1569
|
});
|
|
1570
1570
|
throw new Error("Should have rejected short ID");
|
|
1571
1571
|
} catch (e: any) {
|
|
1572
|
-
if (
|
|
1572
|
+
if (e.reason !== "assessment_id_invalid") throw new Error(`Expected assessment_id_invalid for short ID, got: ${e.reason || e.message}`);
|
|
1573
1573
|
}
|
|
1574
1574
|
});
|
|
1575
1575
|
|
|
@@ -62,7 +62,7 @@ export function getToolDefs(): ToolDef[] {
|
|
|
62
62
|
{
|
|
63
63
|
name: "health_check",
|
|
64
64
|
description:
|
|
65
|
-
"Check MCP catalog consistency, watcher freshness/supervisor state, workspace readiness, and configured agents. Use detail=self_diagnostic for expanded read-only evidence.",
|
|
65
|
+
"Check MCP catalog consistency, watcher freshness/supervisor state, workspace readiness, and configured agents. Agent availability is executable-only unless an explicit provider probe is requested. Use detail=self_diagnostic for expanded read-only evidence.",
|
|
66
66
|
inputSchema: {
|
|
67
67
|
type: "object",
|
|
68
68
|
properties: {
|
|
@@ -78,7 +78,7 @@ export function getToolDefs(): ToolDef[] {
|
|
|
78
78
|
{
|
|
79
79
|
name: "list_agents",
|
|
80
80
|
description:
|
|
81
|
-
"List
|
|
81
|
+
"List agents from the active runtime config, verify executable and model-argument wiring, and report the config path used by this process. provider_status remains not_checked because this read-only check does not contact a model provider or test account balance.",
|
|
82
82
|
inputSchema: {
|
|
83
83
|
type: "object",
|
|
84
84
|
properties: {},
|
|
@@ -416,7 +416,7 @@ export function getToolDefs(): ToolDef[] {
|
|
|
416
416
|
properties: {
|
|
417
417
|
status: {
|
|
418
418
|
type: "string",
|
|
419
|
-
description: "Filter by status: pending, running, done, failed, failed_verification, failed_scope_violation, failed_policy_violation, canceled",
|
|
419
|
+
description: "Filter by status: pending, running, done, failed, failed_verification, failed_scope_violation, failed_policy_violation, timeout, canceled",
|
|
420
420
|
},
|
|
421
421
|
limit: {
|
|
422
422
|
type: "number",
|
|
@@ -140,6 +140,40 @@ function findMdFiles(
|
|
|
140
140
|
return { files: results, truncated };
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
export function extractNpmRunScriptNames(content: string): string[] {
|
|
144
|
+
const names = new Set<string>();
|
|
145
|
+
const collectCommands = (text: string) => {
|
|
146
|
+
const pattern = /\bnpm(?:\.cmd)?\s+run\s+([a-zA-Z0-9:_-]+)/gi;
|
|
147
|
+
let match: RegExpExecArray | null;
|
|
148
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
149
|
+
if (match[1]) names.add(match[1]);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Markdown code blocks are explicit command-bearing regions. Keep their
|
|
154
|
+
// content separate from prose so token boundaries cannot be concatenated.
|
|
155
|
+
const withoutFences = content.replace(/(?:```|~~~)[^\r\n]*\r?\n([\s\S]*?)(?:```|~~~)/g, (_whole, body: string) => {
|
|
156
|
+
collectCommands(body);
|
|
157
|
+
return "\n";
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Inline code is also explicit. Remove it from the prose pass afterward to
|
|
161
|
+
// avoid double-counting and accidental cross-node matches.
|
|
162
|
+
const withoutInlineCode = withoutFences.replace(/`([^`\r\n]+)`/g, (_whole, body: string) => {
|
|
163
|
+
collectCommands(body);
|
|
164
|
+
return " ";
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// In ordinary text, accept only a standalone command line (optionally a
|
|
168
|
+
// list item, shell prompt, or Run:/Command: label). Narrative mentions such
|
|
169
|
+
// as "this guide discusses npm run deploy" are not executable evidence.
|
|
170
|
+
const explicitPlainCommand = /^\s*(?:(?:[-*+]\s+|\d+[.)]\s+|>\s*|\$\s*|PS>\s*)|(?:(?:run|command|命令)\s*[::]\s*))?npm(?:\.cmd)?\s+run\s+[a-zA-Z0-9:_-]+(?:\s+(?:--[^\r\n#]*|#[^\r\n]*))?\s*$/i;
|
|
171
|
+
for (const line of withoutInlineCode.split(/\r?\n/)) {
|
|
172
|
+
if (explicitPlainCommand.test(line)) collectCommands(line);
|
|
173
|
+
}
|
|
174
|
+
return [...names];
|
|
175
|
+
}
|
|
176
|
+
|
|
143
177
|
function readAuditEvidence(path: string): { content: string; truncated: boolean } {
|
|
144
178
|
const half = Math.floor(MAX_AUDIT_EVIDENCE_BYTES / 2);
|
|
145
179
|
const prefix = readTextFilePrefixSync(path, half);
|
|
@@ -515,7 +549,7 @@ export function auditTask(taskId: string): AuditTaskOutput {
|
|
|
515
549
|
};
|
|
516
550
|
// ── 1. Task status ──
|
|
517
551
|
const taskStatus = statusData.status || "unknown";
|
|
518
|
-
const failedStatuses = new Set(["failed", "failed_verification", "failed_scope_violation", "failed_policy_violation", "canceled"]);
|
|
552
|
+
const failedStatuses = new Set(["failed", "failed_verification", "failed_scope_violation", "failed_policy_violation", "canceled", "timeout"]);
|
|
519
553
|
const doneStatuses = new Set(["done", "done_by_agent"]);
|
|
520
554
|
checks.push({
|
|
521
555
|
name: "task_status",
|
|
@@ -729,12 +763,8 @@ export function auditTask(taskId: string): AuditTaskOutput {
|
|
|
729
763
|
continue;
|
|
730
764
|
}
|
|
731
765
|
// Extract npm run xxx
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
const scriptName = ref.replace(/npm\s+run\s+/i, "").replace(/[^a-zA-Z0-9:_-]/g, "");
|
|
735
|
-
if (scriptName)
|
|
736
|
-
allNpmRunRefs.add(scriptName);
|
|
737
|
-
}
|
|
766
|
+
for (const scriptName of extractNpmRunScriptNames(content))
|
|
767
|
+
allNpmRunRefs.add(scriptName);
|
|
738
768
|
// Check release claims
|
|
739
769
|
const claims = scanForReleaseClaims(content);
|
|
740
770
|
for (const c of claims)
|
|
@@ -9,6 +9,7 @@ import type { ToolCatalogSnapshot } from "../catalog/toolCatalog.js";
|
|
|
9
9
|
import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, CHATGPT_SEARCH_TOOL_NAMES, resolveToolProfile } from "../catalog/toolCatalog.js";
|
|
10
10
|
import { readWatcherStatus } from "../../watcherStatus.js";
|
|
11
11
|
import { listTasks } from "../tasks/listTasks.js";
|
|
12
|
+
import { readLastAssessmentValidationFailure } from "../../assessments/assessmentDiagnostics.js";
|
|
12
13
|
|
|
13
14
|
const SERVER_STARTED_AT = Date.now();
|
|
14
15
|
|
|
@@ -130,8 +131,23 @@ export function healthCheck(catalog?: ToolCatalogSnapshot, input: HealthCheckInp
|
|
|
130
131
|
workspace_root: workspace,
|
|
131
132
|
tasks_dir: tasks,
|
|
132
133
|
tunnel,
|
|
134
|
+
config_path: agents.config_path,
|
|
135
|
+
runtime_build_layout: typeof watcher.supervisor?.runtime_build_layout === "string"
|
|
136
|
+
? watcher.supervisor.runtime_build_layout
|
|
137
|
+
: null,
|
|
138
|
+
watcher_entry_path: typeof watcher.supervisor?.watcher_entry_path === "string"
|
|
139
|
+
? watcher.supervisor.watcher_entry_path
|
|
140
|
+
: null,
|
|
141
|
+
validator_module_path: typeof watcher.supervisor?.validator_module_path === "string"
|
|
142
|
+
? watcher.supervisor.validator_module_path
|
|
143
|
+
: null,
|
|
133
144
|
agents: agents.agents,
|
|
134
145
|
agent_status: Object.fromEntries(agents.agents.map((agent) => [agent.name, agent.available ? "ok" : "missing"])),
|
|
146
|
+
agent_readiness: Object.fromEntries(agents.agents.map((agent) => [agent.name, {
|
|
147
|
+
executable_available: agent.available,
|
|
148
|
+
provider_status: agent.provider_status,
|
|
149
|
+
availability_scope: agent.availability_scope,
|
|
150
|
+
}])),
|
|
135
151
|
...(input.detail === "self_diagnostic" ? { self_diagnostic: buildSelfDiagnostic(config) } : {}),
|
|
136
152
|
last_error: tunnel.last_error || (!watcher.available ? watcher.reason : null) || mismatchReport[0] || null,
|
|
137
153
|
};
|
|
@@ -140,7 +156,7 @@ export function healthCheck(catalog?: ToolCatalogSnapshot, input: HealthCheckInp
|
|
|
140
156
|
function buildSelfDiagnostic(config: ReturnType<typeof getConfig>) {
|
|
141
157
|
const recent = listTasks({ limit: 20 });
|
|
142
158
|
const failures = recent.tasks
|
|
143
|
-
.filter((task) => task.status.startsWith("failed") || task.status === "canceled")
|
|
159
|
+
.filter((task) => task.status.startsWith("failed") || task.status === "canceled" || task.status === "timeout")
|
|
144
160
|
.slice(0, 10)
|
|
145
161
|
.map((task) => ({
|
|
146
162
|
task_id: task.task_id,
|
|
@@ -156,6 +172,7 @@ function buildSelfDiagnostic(config: ReturnType<typeof getConfig>) {
|
|
|
156
172
|
configured_agents: Object.keys(config.agents),
|
|
157
173
|
recent_tasks_returned: recent.tasks.length,
|
|
158
174
|
recent_failures: failures,
|
|
175
|
+
last_assessment_validation_failure: readLastAssessmentValidationFailure(),
|
|
159
176
|
};
|
|
160
177
|
const safe = redactSensitiveValue(diagnostic);
|
|
161
178
|
return {
|
|
@@ -182,6 +199,11 @@ function readWatcherSupervisorStatus(): Record<string, unknown> {
|
|
|
182
199
|
restart_attempts: Number.isInteger(Number(raw.restart_attempts)) ? Number(raw.restart_attempts) : 0,
|
|
183
200
|
last_error: safeText(raw.last_error, 500) || null,
|
|
184
201
|
checked_at: safeText(raw.checked_at, 80) || null,
|
|
202
|
+
project_root: safeText(raw.project_root, 500) || null,
|
|
203
|
+
config_path: safeText(raw.config_path, 500) || null,
|
|
204
|
+
watcher_entry_path: safeText(raw.watcher_entry_path, 500) || null,
|
|
205
|
+
validator_module_path: safeText(raw.validator_module_path, 500) || null,
|
|
206
|
+
runtime_build_layout: safeText(raw.runtime_build_layout, 80) || null,
|
|
185
207
|
};
|
|
186
208
|
} catch {
|
|
187
209
|
return { observed: true, managed: false, status: "invalid_status_file" };
|
|
@@ -242,6 +264,12 @@ function readTunnelStatus(): Record<string, unknown> & { last_error: string | nu
|
|
|
242
264
|
: [],
|
|
243
265
|
tool_manifest_sha256: safeText(raw.tool_manifest_sha256, 80) || null,
|
|
244
266
|
core_tools_ready: Boolean(raw.core_tools_ready),
|
|
267
|
+
project_root: safeText(raw.project_root, 500) || null,
|
|
268
|
+
config_path: safeText(raw.config_path, 500) || null,
|
|
269
|
+
mcp_entry_path: safeText(raw.mcp_entry_path, 500) || null,
|
|
270
|
+
watcher_entry_path: safeText(raw.watcher_entry_path, 500) || null,
|
|
271
|
+
validator_module_path: safeText(raw.validator_module_path, 500) || null,
|
|
272
|
+
runtime_build_layout: safeText(raw.runtime_build_layout, 80) || null,
|
|
245
273
|
};
|
|
246
274
|
const redacted = redactSensitiveContent(JSON.stringify(allowed));
|
|
247
275
|
return JSON.parse(redacted.content);
|
|
@@ -5,6 +5,7 @@ import { guardReadPath } from "../../security/pathGuard.js";
|
|
|
5
5
|
import { writeTaskProgress } from "../../runner/taskProgress.js";
|
|
6
6
|
import { mutateTaskStatus } from "../../runner/taskStatusStore.js";
|
|
7
7
|
import type { TaskStatus } from "./createTask.js";
|
|
8
|
+
import { isTerminalTaskStatus } from "./taskStates.js";
|
|
8
9
|
|
|
9
10
|
export interface TaskTerminationResponse {
|
|
10
11
|
task_id: string;
|
|
@@ -38,7 +39,7 @@ export function requestTaskTermination(taskId: string, force: boolean) {
|
|
|
38
39
|
const now = new Date().toISOString();
|
|
39
40
|
const outcome = mutateTaskStatus<TaskTerminationOutcome>(statusFile, (current) => {
|
|
40
41
|
const currentStatus = current.status as TaskStatus;
|
|
41
|
-
if (
|
|
42
|
+
if (isTerminalTaskStatus(currentStatus)) {
|
|
42
43
|
return { result: {
|
|
43
44
|
response: {
|
|
44
45
|
task_id: taskId,
|
|
@@ -12,12 +12,15 @@ import {
|
|
|
12
12
|
createAssessment,
|
|
13
13
|
readAssessment,
|
|
14
14
|
validateAssessmentFreshness,
|
|
15
|
+
markAssessmentUsed,
|
|
15
16
|
generateAssessmentId,
|
|
16
17
|
createAssessmentDir,
|
|
17
18
|
type AssessmentRecord,
|
|
18
19
|
type AgentAssessmentSummary,
|
|
19
20
|
} from "../../assessments/assessmentStore.js";
|
|
20
21
|
import { runAgentAssessment } from "../../assessments/agentAssessor.js";
|
|
22
|
+
import { recordAssessmentValidationFailure } from "../../assessments/assessmentDiagnostics.js";
|
|
23
|
+
import { ASSESSMENT_SECURITY_SNAPSHOT_VERSION } from "../../assessments/securitySnapshot.js";
|
|
21
24
|
import { captureRepoSnapshot } from "../../runner/changeCapture.js";
|
|
22
25
|
import { writeTaskProgress } from "../../runner/taskProgress.js";
|
|
23
26
|
import { PatchWardenError } from "../../errors.js";
|
|
@@ -28,7 +31,7 @@ import {
|
|
|
28
31
|
type ChangePolicy,
|
|
29
32
|
type TaskTemplateName,
|
|
30
33
|
} from "../taskTemplates.js";
|
|
31
|
-
import { PATCHWARDEN_VERSION, ARTIFACT_SCHEMA_VERSION } from "../../version.js";
|
|
34
|
+
import { PATCHWARDEN_VERSION, ARTIFACT_SCHEMA_VERSION, TOOL_SCHEMA_EPOCH } from "../../version.js";
|
|
32
35
|
import { getLastToolCatalogSnapshot, resolveToolProfile } from "../catalog/toolCatalog.js";
|
|
33
36
|
import {
|
|
34
37
|
derivePendingReason,
|
|
@@ -43,7 +46,10 @@ import { atomicWriteJsonFileSync } from "../../utils/atomicFile.js";
|
|
|
43
46
|
export type TaskStatus =
|
|
44
47
|
| "pending"
|
|
45
48
|
| "running"
|
|
49
|
+
| "executing_agent"
|
|
50
|
+
| "verifying"
|
|
46
51
|
| "collecting_artifacts"
|
|
52
|
+
| "cancel_requested"
|
|
47
53
|
| "done_by_agent" // v0.7.0: agent self-reported done or status reconciled to done; acceptance_status defaults to "pending"
|
|
48
54
|
| "accepted" // v0.7.2: audit_task confirmed acceptance
|
|
49
55
|
| "rejected" // v0.7.2: audit_task confirmed rejection
|
|
@@ -56,6 +62,7 @@ export type TaskStatus =
|
|
|
56
62
|
| "failed_policy_violation"
|
|
57
63
|
| "failed_stale" // v0.7.0: process dead / heartbeat expired
|
|
58
64
|
| "orphaned" // v0.7.0: watcher no longer owns the task
|
|
65
|
+
| "timeout"
|
|
59
66
|
| "canceled";
|
|
60
67
|
|
|
61
68
|
/**
|
|
@@ -83,6 +90,7 @@ export type TaskPhase =
|
|
|
83
90
|
| "failed_policy_violation"
|
|
84
91
|
| "failed_stale" // v0.7.0
|
|
85
92
|
| "orphaned" // v0.7.0
|
|
93
|
+
| "timeout"
|
|
86
94
|
| "done_by_agent" // v0.7.0
|
|
87
95
|
| "accepted" // v0.7.2
|
|
88
96
|
| "rejected" // v0.7.2
|
|
@@ -205,6 +213,15 @@ export async function createTask(input: CreateTaskInput): Promise<CreateTaskResu
|
|
|
205
213
|
let effectiveInput = input;
|
|
206
214
|
if (executionMode === "execute" && input.assessment_id) {
|
|
207
215
|
assessmentRecord = readAssessment(input.assessment_id);
|
|
216
|
+
if (assessmentRecord.used_at) {
|
|
217
|
+
throw new PatchWardenError(
|
|
218
|
+
"assessment_used",
|
|
219
|
+
`Assessment "${input.assessment_id}" has already been used.`,
|
|
220
|
+
"Run create_task with execution_mode=assess_only again to obtain a new assessment_id.",
|
|
221
|
+
true,
|
|
222
|
+
{ assessment_id: input.assessment_id, used_at: assessmentRecord.used_at },
|
|
223
|
+
);
|
|
224
|
+
}
|
|
208
225
|
// Parameter mismatch check: if ChatGPT passes params that differ from the assessment, reject
|
|
209
226
|
effectiveInput = mergeAssessmentIntoInput(input, assessmentRecord);
|
|
210
227
|
}
|
|
@@ -490,6 +507,10 @@ export async function createTask(input: CreateTaskInput): Promise<CreateTaskResu
|
|
|
490
507
|
agent: effectiveInput.agent,
|
|
491
508
|
timeout_seconds: timeoutSeconds,
|
|
492
509
|
change_policy: changePolicy,
|
|
510
|
+
scope: effectiveInput.scope,
|
|
511
|
+
forbidden: effectiveInput.forbidden,
|
|
512
|
+
verification: effectiveInput.verification,
|
|
513
|
+
done_evidence: effectiveInput.done_evidence,
|
|
493
514
|
snapshot,
|
|
494
515
|
...(preGeneratedAssessmentId ? { assessment_id: preGeneratedAssessmentId } : {}),
|
|
495
516
|
...(preGeneratedAssessmentDir ? { assessment_dir: preGeneratedAssessmentDir } : {}),
|
|
@@ -585,14 +606,42 @@ export async function createTask(input: CreateTaskInput): Promise<CreateTaskResu
|
|
|
585
606
|
const snapshot = await captureRepoSnapshot(safeRepoPath);
|
|
586
607
|
const validation = validateAssessmentFreshness(input.assessment_id!, snapshot);
|
|
587
608
|
if (!validation.valid) {
|
|
609
|
+
recordAssessmentValidationFailure(validation);
|
|
588
610
|
throw new PatchWardenError(
|
|
589
611
|
validation.failure_reason || "assessment_validation_failed",
|
|
590
612
|
`Assessment "${input.assessment_id}" is no longer valid: ${validation.failure_reason}`,
|
|
591
613
|
"Re-run create_task with execution_mode=assess_only to get a fresh assessment_id.",
|
|
592
614
|
true,
|
|
593
|
-
{
|
|
615
|
+
{
|
|
616
|
+
assessment_id: input.assessment_id,
|
|
617
|
+
reason_code: validation.failure_reason,
|
|
618
|
+
failure_reason: validation.failure_reason,
|
|
619
|
+
...(validation.expected_hash ? { expected_hash: validation.expected_hash } : {}),
|
|
620
|
+
...(validation.actual_hash ? { actual_hash: validation.actual_hash } : {}),
|
|
621
|
+
...(validation.expected_hash ? { assessment_config_sha256: validation.expected_hash } : {}),
|
|
622
|
+
...(validation.actual_hash ? { current_config_sha256: validation.actual_hash } : {}),
|
|
623
|
+
snapshot_version: validation.assessment?.assessment_security_snapshot_version || null,
|
|
624
|
+
assessment_created_at: validation.assessment?.created_at || null,
|
|
625
|
+
assessment_expires_at: validation.assessment?.expires_at || null,
|
|
626
|
+
config_changed: validation.failure_reason === "assessment_stale_config",
|
|
627
|
+
...(validation.config_change_categories
|
|
628
|
+
? {
|
|
629
|
+
config_change_categories: validation.config_change_categories,
|
|
630
|
+
changed_field_names: validation.config_change_categories,
|
|
631
|
+
changed_config_sections: validation.config_change_categories,
|
|
632
|
+
}
|
|
633
|
+
: {}),
|
|
634
|
+
assessment_fingerprint_version: validation.assessment?.assessment_security_snapshot_version || null,
|
|
635
|
+
current_fingerprint_version: ASSESSMENT_SECURITY_SNAPSHOT_VERSION,
|
|
636
|
+
assessment_schema_epoch: validation.assessment?.assessment_schema_epoch || null,
|
|
637
|
+
current_schema_epoch: TOOL_SCHEMA_EPOCH,
|
|
638
|
+
validator_module_path: "dist/assessments/assessmentStore.js",
|
|
639
|
+
validator_build_id: `patchwarden-${PATCHWARDEN_VERSION}:${TOOL_SCHEMA_EPOCH}:${ASSESSMENT_SECURITY_SNAPSHOT_VERSION}`,
|
|
640
|
+
watcher_instance_id: process.env.PATCHWARDEN_WATCHER_INSTANCE_ID || null,
|
|
641
|
+
}
|
|
594
642
|
);
|
|
595
643
|
}
|
|
644
|
+
markAssessmentUsed(input.assessment_id!);
|
|
596
645
|
}
|
|
597
646
|
|
|
598
647
|
const { taskId, taskDir } = createTaskDirectory(tasksDir, config.workspaceRoot, config.tasksDir);
|
|
@@ -700,18 +749,22 @@ function mergeAssessmentIntoInput(
|
|
|
700
749
|
// Do NOT set template/inline_plan alongside plan_id — createTask requires exactly one plan source.
|
|
701
750
|
const merged: CreateTaskInput = {
|
|
702
751
|
...input,
|
|
703
|
-
plan_id: record.plan_id ||
|
|
752
|
+
plan_id: record.plan_id || undefined,
|
|
704
753
|
template: undefined,
|
|
705
754
|
inline_plan: undefined,
|
|
706
|
-
goal: record.goal ||
|
|
755
|
+
goal: record.goal || undefined,
|
|
707
756
|
agent: record.agent,
|
|
708
757
|
repo_path: record.repo_path,
|
|
709
758
|
test_command: record.test_command || undefined,
|
|
710
|
-
verify_commands: record.verify_commands ||
|
|
711
|
-
timeout_seconds: record.timeout_seconds
|
|
759
|
+
verify_commands: record.verify_commands || [],
|
|
760
|
+
timeout_seconds: record.timeout_seconds,
|
|
761
|
+
scope: record.scope || [],
|
|
762
|
+
forbidden: record.forbidden || [],
|
|
763
|
+
verification: record.verification || [],
|
|
764
|
+
done_evidence: record.done_evidence || [],
|
|
712
765
|
};
|
|
713
766
|
// Parameter mismatch check: if caller passed explicit params that differ from assessment
|
|
714
|
-
if (input.template &&
|
|
767
|
+
if (input.template && input.template !== record.template) {
|
|
715
768
|
throw new PatchWardenError(
|
|
716
769
|
"assessment_parameter_mismatch",
|
|
717
770
|
`template mismatch: caller passed "${input.template}" but assessment has "${record.template}".`,
|
|
@@ -720,7 +773,7 @@ function mergeAssessmentIntoInput(
|
|
|
720
773
|
{ field: "template", assessment_value: record.template, caller_value: input.template }
|
|
721
774
|
);
|
|
722
775
|
}
|
|
723
|
-
if (input.goal &&
|
|
776
|
+
if (input.goal && input.goal !== record.goal) {
|
|
724
777
|
throw new PatchWardenError(
|
|
725
778
|
"assessment_parameter_mismatch",
|
|
726
779
|
`goal mismatch: caller passed a different goal than the assessment.`,
|
|
@@ -738,6 +791,58 @@ function mergeAssessmentIntoInput(
|
|
|
738
791
|
{ field: "repo_path", assessment_value: record.repo_path, caller_value: input.repo_path }
|
|
739
792
|
);
|
|
740
793
|
}
|
|
794
|
+
if (input.agent && input.agent !== record.agent) {
|
|
795
|
+
throw new PatchWardenError(
|
|
796
|
+
"assessment_parameter_mismatch",
|
|
797
|
+
`agent mismatch: caller passed "${input.agent}" but assessment has "${record.agent}".`,
|
|
798
|
+
"Do not override assessment-locked parameters. Use the same assessment_id as-is.",
|
|
799
|
+
true,
|
|
800
|
+
{ field: "agent" },
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
if (
|
|
804
|
+
input.verify_commands
|
|
805
|
+
&& JSON.stringify(input.verify_commands) !== JSON.stringify(record.verify_commands || [])
|
|
806
|
+
) {
|
|
807
|
+
throw new PatchWardenError(
|
|
808
|
+
"assessment_parameter_mismatch",
|
|
809
|
+
"verify_commands mismatch: caller passed commands different from the assessment.",
|
|
810
|
+
"Do not override assessment-locked parameters. Use the same assessment_id as-is.",
|
|
811
|
+
true,
|
|
812
|
+
{ field: "verify_commands" },
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
if (input.test_command && input.test_command !== record.test_command) {
|
|
816
|
+
throw new PatchWardenError(
|
|
817
|
+
"assessment_parameter_mismatch",
|
|
818
|
+
"test_command mismatch: caller passed a command different from the assessment.",
|
|
819
|
+
"Do not override assessment-locked parameters. Use the same assessment_id as-is.",
|
|
820
|
+
true,
|
|
821
|
+
{ field: "test_command" },
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
if (input.timeout_seconds !== undefined && input.timeout_seconds !== record.timeout_seconds) {
|
|
825
|
+
throw new PatchWardenError(
|
|
826
|
+
"assessment_parameter_mismatch",
|
|
827
|
+
"timeout_seconds mismatch: caller passed a timeout different from the assessment.",
|
|
828
|
+
"Do not override assessment-locked parameters. Use the same assessment_id as-is.",
|
|
829
|
+
true,
|
|
830
|
+
{ field: "timeout_seconds" },
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
for (const field of ["scope", "forbidden", "verification", "done_evidence"] as const) {
|
|
834
|
+
const callerValue = input[field];
|
|
835
|
+
const assessmentValue = record[field] || [];
|
|
836
|
+
if (callerValue && JSON.stringify(callerValue) !== JSON.stringify(assessmentValue)) {
|
|
837
|
+
throw new PatchWardenError(
|
|
838
|
+
"assessment_parameter_mismatch",
|
|
839
|
+
`${field} mismatch: caller passed values different from the assessment.`,
|
|
840
|
+
"Do not override assessment-locked parameters. Use the same assessment_id as-is.",
|
|
841
|
+
true,
|
|
842
|
+
{ field },
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
741
846
|
return merged;
|
|
742
847
|
}
|
|
743
848
|
|
|
@@ -7,6 +7,7 @@ import { redactSensitiveContent } from "../../security/contentRedaction.js";
|
|
|
7
7
|
import { readTextFileTailLinesSync } from "../../utils/boundedFile.js";
|
|
8
8
|
import { readTaskRuntime } from "../../runner/taskRuntime.js";
|
|
9
9
|
import { isWatcherOwningTask, readWatcherInstanceId } from "../../watcherStatus.js";
|
|
10
|
+
import { isActiveTaskStatus, isTerminalTaskStatus } from "./taskStates.js";
|
|
10
11
|
|
|
11
12
|
// ── v0.7.0: Task diagnosis types ──────────────────────────────────
|
|
12
13
|
|
|
@@ -153,17 +154,12 @@ export function diagnoseTask(
|
|
|
153
154
|
const phaseStr = (runtime.phase || (typeof status.phase === "string" ? status.phase : "queued")) as string;
|
|
154
155
|
|
|
155
156
|
// ── Terminal tasks: no diagnosis needed ──
|
|
156
|
-
|
|
157
|
-
"done", "done_by_agent", "failed", "failed_verification",
|
|
158
|
-
"failed_scope_violation", "failed_policy_violation",
|
|
159
|
-
"failed_stale", "orphaned", "canceled",
|
|
160
|
-
]);
|
|
161
|
-
if (terminalStatuses.has(statusStr)) {
|
|
157
|
+
if (isTerminalTaskStatus(statusStr)) {
|
|
162
158
|
return buildTerminalDiagnosis(taskId, statusStr, phaseStr);
|
|
163
159
|
}
|
|
164
160
|
|
|
165
|
-
//
|
|
166
|
-
if (statusStr
|
|
161
|
+
// Diagnose both current status spellings and legacy phase-as-status records.
|
|
162
|
+
if (!isActiveTaskStatus(statusStr)) {
|
|
167
163
|
return buildUnknownDiagnosis(taskId, statusStr, phaseStr, [`unexpected status "${statusStr}" for diagnosis`]);
|
|
168
164
|
}
|
|
169
165
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type PendingReason,
|
|
13
13
|
type WatcherStatusSnapshot,
|
|
14
14
|
} from "../../watcherStatus.js";
|
|
15
|
+
import { isTerminalTaskStatus } from "./taskStates.js";
|
|
15
16
|
|
|
16
17
|
export interface GetTaskStatusOutput {
|
|
17
18
|
task_id: string;
|
|
@@ -69,7 +70,11 @@ export function getTaskStatus(taskId: string): GetTaskStatusOutput {
|
|
|
69
70
|
const raw = readFileSync(statusFile, "utf-8");
|
|
70
71
|
const status = JSON.parse(raw) as GetTaskStatusOutput;
|
|
71
72
|
const runtime = readTaskRuntime(taskDir);
|
|
72
|
-
|
|
73
|
+
// A terminal status is authoritative. A stale runtime phase from a crashed
|
|
74
|
+
// Runner must never make a reconciled task look active again.
|
|
75
|
+
const phase = isTerminalTaskStatus(String(status.status))
|
|
76
|
+
? status.phase || "queued"
|
|
77
|
+
: runtime.phase || status.phase || "queued";
|
|
73
78
|
const watcher = readWatcherStatus(config);
|
|
74
79
|
const pendingReason = derivePendingReason({ status: status.status, phase }, watcher);
|
|
75
80
|
return {
|
|
@@ -8,16 +8,7 @@ import {
|
|
|
8
8
|
readTextFilePrefixSync,
|
|
9
9
|
readTextFileTailLinesSync,
|
|
10
10
|
} from "../../utils/boundedFile.js";
|
|
11
|
-
|
|
12
|
-
const TERMINAL_STATUSES = new Set([
|
|
13
|
-
"done",
|
|
14
|
-
"done_by_agent",
|
|
15
|
-
"failed",
|
|
16
|
-
"failed_verification",
|
|
17
|
-
"failed_scope_violation",
|
|
18
|
-
"failed_policy_violation",
|
|
19
|
-
"canceled",
|
|
20
|
-
]);
|
|
11
|
+
import { isTerminalTaskStatus } from "./taskStates.js";
|
|
21
12
|
|
|
22
13
|
export interface TaskSummaryOutput {
|
|
23
14
|
task_id: string;
|
|
@@ -143,7 +134,7 @@ export function getTaskSummary(taskId: string, options: GetTaskSummaryOptions =
|
|
|
143
134
|
const verifyRead = tryReadJson(join(taskDir, "verify.json"));
|
|
144
135
|
const result = resultRead.data;
|
|
145
136
|
const verify = verifyRead.data;
|
|
146
|
-
const terminal =
|
|
137
|
+
const terminal = isTerminalTaskStatus(String(status.status));
|
|
147
138
|
// Phase 4: Use new_out_of_scope_changes (task-caused) for acceptance status.
|
|
148
139
|
// Pre-existing external dirty files that didn't change should NOT fail acceptance.
|
|
149
140
|
const outOfScope = asArray(
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type WatcherState,
|
|
12
12
|
type WatcherStatusSnapshot,
|
|
13
13
|
} from "../../watcherStatus.js";
|
|
14
|
+
import { isActiveTaskStatus } from "./taskStates.js";
|
|
14
15
|
|
|
15
16
|
export interface TaskEntry {
|
|
16
17
|
task_id: string;
|
|
@@ -97,7 +98,7 @@ export function listTasks(input?: ListTasksInput): ListTasksOutput {
|
|
|
97
98
|
: null;
|
|
98
99
|
if (taskAcceptance !== filterAcceptance) continue;
|
|
99
100
|
}
|
|
100
|
-
if (input?.active_only && !
|
|
101
|
+
if (input?.active_only && !isActiveTaskStatus(String(data.status))) continue;
|
|
101
102
|
const normalizedRepo = String(data.repo_path || ".").replace(/\\/g, "/");
|
|
102
103
|
const normalizedResolvedRepo = String(data.resolved_repo_path || "").replace(/\\/g, "/");
|
|
103
104
|
if (filterRepo && normalizedRepo !== filterRepo && normalizedResolvedRepo !== filterRepo) continue;
|