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/dist/runner/watch.js
CHANGED
|
@@ -28,6 +28,8 @@ import { mutateTaskStatus } from "./taskStatusStore.js";
|
|
|
28
28
|
import { logger } from "../logging.js";
|
|
29
29
|
import { redactSensitiveContent } from "../security/contentRedaction.js";
|
|
30
30
|
import { atomicWriteFileSync } from "../utils/atomicFile.js";
|
|
31
|
+
import { reconcileTasks } from "../tools/tasks/reconcileTasks.js";
|
|
32
|
+
import { buildAssessmentValidationFailureDiagnostic, recordAssessmentValidationFailure, } from "../assessments/assessmentDiagnostics.js";
|
|
31
33
|
// Note: `runTask` is imported lazily inside `tick()` via `await import("./runTask.js")`.
|
|
32
34
|
// This keeps `import { acquireWatcherLock }` side-effect-free for unit tests and
|
|
33
35
|
// avoids eagerly pulling in the full tools/dispatch graph at module load.
|
|
@@ -47,6 +49,11 @@ const WATCHER_LAUNCHER_PID = Number.isInteger(Number(process.env.PATCHWARDEN_WAT
|
|
|
47
49
|
// Track executed tasks to prevent re-execution
|
|
48
50
|
const executedTasks = new Set();
|
|
49
51
|
let consecutiveTickFailures = 0;
|
|
52
|
+
let activeTaskId = null;
|
|
53
|
+
let activeTaskPhase = null;
|
|
54
|
+
let tickRunning = false;
|
|
55
|
+
let tickStartedAt = null;
|
|
56
|
+
let lastTickCompletedAt = null;
|
|
50
57
|
// ── Single-instance lock ─────────────────────────────────────────
|
|
51
58
|
// watch.ts is a long-running poller; without a lock, two watchers started
|
|
52
59
|
// against the same workspace would race on the same pending tasks. The lock
|
|
@@ -184,6 +191,8 @@ export function createNonOverlappingRunner(operation, onSkipped = () => undefine
|
|
|
184
191
|
}
|
|
185
192
|
// ── Main loop ─────────────────────────────────────────────────────
|
|
186
193
|
async function tick() {
|
|
194
|
+
tickRunning = true;
|
|
195
|
+
tickStartedAt = new Date().toISOString();
|
|
187
196
|
try {
|
|
188
197
|
writeWatcherHeartbeat("running");
|
|
189
198
|
// Ensure tasks directory exists
|
|
@@ -192,6 +201,7 @@ async function tick() {
|
|
|
192
201
|
writeWatcherHeartbeat("running");
|
|
193
202
|
return;
|
|
194
203
|
}
|
|
204
|
+
reconcileInterruptedTasks();
|
|
195
205
|
const entries = readdirSync(tasksDir, { withFileTypes: true });
|
|
196
206
|
const taskDirs = entries.filter((e) => e.isDirectory());
|
|
197
207
|
for (const entry of taskDirs) {
|
|
@@ -245,7 +255,9 @@ async function tick() {
|
|
|
245
255
|
const preExecSnapshot = await captureRepoSnapshot(resolvedRepoPath);
|
|
246
256
|
const validation = validateAssessmentFreshness(statusData.assessment_id, preExecSnapshot);
|
|
247
257
|
if (!validation.valid) {
|
|
248
|
-
|
|
258
|
+
recordAssessmentValidationFailure(validation);
|
|
259
|
+
const diagnostic = buildAssessmentValidationFailureDiagnostic(validation);
|
|
260
|
+
throw new Error(`assessment validation failed: ${JSON.stringify(diagnostic)}`);
|
|
249
261
|
}
|
|
250
262
|
}
|
|
251
263
|
}
|
|
@@ -276,14 +288,22 @@ async function tick() {
|
|
|
276
288
|
// ── Execute ──
|
|
277
289
|
logger.info(`[watcher] Executing: ${taskId}`);
|
|
278
290
|
executedTasks.add(taskId);
|
|
291
|
+
activeTaskId = taskId;
|
|
292
|
+
activeTaskPhase = "executing_agent";
|
|
279
293
|
try {
|
|
280
294
|
const { runTask } = await import("./runTask.js");
|
|
281
295
|
const result = await runTask(taskId);
|
|
296
|
+
activeTaskPhase = result.status;
|
|
282
297
|
logger.info(`[watcher] ${taskId} → ${result.status}`);
|
|
283
298
|
}
|
|
284
299
|
catch (err) {
|
|
300
|
+
activeTaskPhase = "error";
|
|
285
301
|
logger.error(`[watcher] ${taskId} → error: ${err instanceof Error ? err.message : String(err)}`);
|
|
286
302
|
}
|
|
303
|
+
finally {
|
|
304
|
+
activeTaskId = null;
|
|
305
|
+
activeTaskPhase = null;
|
|
306
|
+
}
|
|
287
307
|
}
|
|
288
308
|
consecutiveTickFailures = 0;
|
|
289
309
|
writeWatcherHeartbeat("running");
|
|
@@ -301,6 +321,26 @@ async function tick() {
|
|
|
301
321
|
});
|
|
302
322
|
}
|
|
303
323
|
}
|
|
324
|
+
finally {
|
|
325
|
+
tickRunning = false;
|
|
326
|
+
lastTickCompletedAt = new Date().toISOString();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function reconcileInterruptedTasks() {
|
|
330
|
+
const result = reconcileTasks({
|
|
331
|
+
mode: "safe_fix",
|
|
332
|
+
max_age_minutes: Math.max(config.watcherStaleSeconds / 60, 1 / 60),
|
|
333
|
+
include_done_candidates: true,
|
|
334
|
+
}, config);
|
|
335
|
+
if (result.reconciled > 0) {
|
|
336
|
+
logger.warn("[watcher] Reconciled tasks left by a previous runner", {
|
|
337
|
+
reconciled: result.reconciled,
|
|
338
|
+
task_ids: result.reports
|
|
339
|
+
.filter((report) => report.action_taken !== "left_unchanged")
|
|
340
|
+
.map((report) => report.task_id)
|
|
341
|
+
.slice(0, 20),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
304
344
|
}
|
|
305
345
|
function writeWatcherHeartbeat(status, lastError) {
|
|
306
346
|
atomicWriteFileSync(WATCHER_HEARTBEAT_FILE, JSON.stringify({
|
|
@@ -312,8 +352,29 @@ function writeWatcherHeartbeat(status, lastError) {
|
|
|
312
352
|
launcher_pid: WATCHER_LAUNCHER_PID,
|
|
313
353
|
consecutive_failures: consecutiveTickFailures,
|
|
314
354
|
last_error: lastError ? lastError.slice(0, 500) : null,
|
|
355
|
+
heartbeat_source: "independent_interval",
|
|
356
|
+
tick_running: tickRunning,
|
|
357
|
+
tick_started_at: tickStartedAt,
|
|
358
|
+
last_tick_completed_at: lastTickCompletedAt,
|
|
359
|
+
active_task_id: activeTaskId,
|
|
360
|
+
active_task_phase: activeTaskPhase,
|
|
315
361
|
}, null, 2));
|
|
316
362
|
}
|
|
363
|
+
function startWatcherHeartbeatLoop() {
|
|
364
|
+
const intervalMs = Math.min(2000, Math.max(1000, Math.floor(config.watcherStaleSeconds * 1000 / 3)));
|
|
365
|
+
const timer = setInterval(() => {
|
|
366
|
+
try {
|
|
367
|
+
writeWatcherHeartbeat("running");
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
logger.warn("[watcher] Independent heartbeat write failed", {
|
|
371
|
+
error: error instanceof Error ? error.message : String(error),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}, intervalMs);
|
|
375
|
+
timer.unref?.();
|
|
376
|
+
return timer;
|
|
377
|
+
}
|
|
317
378
|
// ── Start (only when executed as a script) ─────────────────────────
|
|
318
379
|
// The bootstrap below has process-wide side effects (mkdir, signal handlers,
|
|
319
380
|
// polling interval, lock acquisition). Guarding it with a main-module check
|
|
@@ -338,6 +399,7 @@ if (isMainModule) {
|
|
|
338
399
|
throw err;
|
|
339
400
|
}
|
|
340
401
|
logger.info("[watcher] Started");
|
|
402
|
+
const heartbeatTimer = startWatcherHeartbeatLoop();
|
|
341
403
|
const scheduledTick = createNonOverlappingRunner(tick, () => {
|
|
342
404
|
logger.warn("[watcher] Previous tick is still running; skipping overlapping poll");
|
|
343
405
|
});
|
|
@@ -347,6 +409,7 @@ if (isMainModule) {
|
|
|
347
409
|
// Graceful shutdown
|
|
348
410
|
function shutdown() {
|
|
349
411
|
logger.info("[watcher] Stopped");
|
|
412
|
+
clearInterval(heartbeatTimer);
|
|
350
413
|
try {
|
|
351
414
|
releaseWatcherLock(WATCHER_LOCK_FILE);
|
|
352
415
|
}
|
package/dist/smoke-test.js
CHANGED
|
@@ -1269,7 +1269,7 @@ await test("K8. assessment not found rejected", async () => {
|
|
|
1269
1269
|
agent: "codex",
|
|
1270
1270
|
repo_path: ".",
|
|
1271
1271
|
execution_mode: "execute",
|
|
1272
|
-
assessment_id: "
|
|
1272
|
+
assessment_id: "assessment_20260101_000000_" + "0".repeat(32),
|
|
1273
1273
|
});
|
|
1274
1274
|
throw new Error("Should have rejected unknown assessment");
|
|
1275
1275
|
}
|
|
@@ -1509,8 +1509,8 @@ await test("K16. assessment_short_id cannot execute", async () => {
|
|
|
1509
1509
|
throw new Error("Should have rejected short ID");
|
|
1510
1510
|
}
|
|
1511
1511
|
catch (e) {
|
|
1512
|
-
if (
|
|
1513
|
-
throw new Error(`Expected
|
|
1512
|
+
if (e.reason !== "assessment_id_invalid")
|
|
1513
|
+
throw new Error(`Expected assessment_id_invalid for short ID, got: ${e.reason || e.message}`);
|
|
1514
1514
|
}
|
|
1515
1515
|
});
|
|
1516
1516
|
await test("K17. blocked assessment cannot execute", async () => {
|
|
@@ -44,7 +44,7 @@ export function getToolDefs() {
|
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
name: "health_check",
|
|
47
|
-
description: "Check MCP catalog consistency, watcher freshness/supervisor state, workspace readiness, and configured agents. Use detail=self_diagnostic for expanded read-only evidence.",
|
|
47
|
+
description: "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.",
|
|
48
48
|
inputSchema: {
|
|
49
49
|
type: "object",
|
|
50
50
|
properties: {
|
|
@@ -59,7 +59,7 @@ export function getToolDefs() {
|
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
61
|
name: "list_agents",
|
|
62
|
-
description: "List
|
|
62
|
+
description: "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.",
|
|
63
63
|
inputSchema: {
|
|
64
64
|
type: "object",
|
|
65
65
|
properties: {},
|
|
@@ -388,7 +388,7 @@ export function getToolDefs() {
|
|
|
388
388
|
properties: {
|
|
389
389
|
status: {
|
|
390
390
|
type: "string",
|
|
391
|
-
description: "Filter by status: pending, running, done, failed, failed_verification, failed_scope_violation, failed_policy_violation, canceled",
|
|
391
|
+
description: "Filter by status: pending, running, done, failed, failed_verification, failed_scope_violation, failed_policy_violation, timeout, canceled",
|
|
392
392
|
},
|
|
393
393
|
limit: {
|
|
394
394
|
type: "number",
|
|
@@ -33,6 +33,7 @@ export interface AuditTaskOutput {
|
|
|
33
33
|
recommended_next_actions: string[];
|
|
34
34
|
}
|
|
35
35
|
import type { ChangedFile } from "../../runner/changeCapture.js";
|
|
36
|
+
export declare function extractNpmRunScriptNames(content: string): string[];
|
|
36
37
|
/**
|
|
37
38
|
* Check 1: forbidden_scope_violation (fail level).
|
|
38
39
|
* Returns null when no forbidden patterns are configured (skip).
|
|
@@ -73,6 +73,38 @@ function findMdFiles(dir, maxDepth = 3, maxFiles = MAX_AUDIT_DOC_FILES) {
|
|
|
73
73
|
truncated ||= pending.length > 0 || results.length >= maxFiles || visitedDirectories >= maxDirectories;
|
|
74
74
|
return { files: results, truncated };
|
|
75
75
|
}
|
|
76
|
+
export function extractNpmRunScriptNames(content) {
|
|
77
|
+
const names = new Set();
|
|
78
|
+
const collectCommands = (text) => {
|
|
79
|
+
const pattern = /\bnpm(?:\.cmd)?\s+run\s+([a-zA-Z0-9:_-]+)/gi;
|
|
80
|
+
let match;
|
|
81
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
82
|
+
if (match[1])
|
|
83
|
+
names.add(match[1]);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
// Markdown code blocks are explicit command-bearing regions. Keep their
|
|
87
|
+
// content separate from prose so token boundaries cannot be concatenated.
|
|
88
|
+
const withoutFences = content.replace(/(?:```|~~~)[^\r\n]*\r?\n([\s\S]*?)(?:```|~~~)/g, (_whole, body) => {
|
|
89
|
+
collectCommands(body);
|
|
90
|
+
return "\n";
|
|
91
|
+
});
|
|
92
|
+
// Inline code is also explicit. Remove it from the prose pass afterward to
|
|
93
|
+
// avoid double-counting and accidental cross-node matches.
|
|
94
|
+
const withoutInlineCode = withoutFences.replace(/`([^`\r\n]+)`/g, (_whole, body) => {
|
|
95
|
+
collectCommands(body);
|
|
96
|
+
return " ";
|
|
97
|
+
});
|
|
98
|
+
// In ordinary text, accept only a standalone command line (optionally a
|
|
99
|
+
// list item, shell prompt, or Run:/Command: label). Narrative mentions such
|
|
100
|
+
// as "this guide discusses npm run deploy" are not executable evidence.
|
|
101
|
+
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;
|
|
102
|
+
for (const line of withoutInlineCode.split(/\r?\n/)) {
|
|
103
|
+
if (explicitPlainCommand.test(line))
|
|
104
|
+
collectCommands(line);
|
|
105
|
+
}
|
|
106
|
+
return [...names];
|
|
107
|
+
}
|
|
76
108
|
function readAuditEvidence(path) {
|
|
77
109
|
const half = Math.floor(MAX_AUDIT_EVIDENCE_BYTES / 2);
|
|
78
110
|
const prefix = readTextFilePrefixSync(path, half);
|
|
@@ -466,7 +498,7 @@ export function auditTask(taskId) {
|
|
|
466
498
|
};
|
|
467
499
|
// ── 1. Task status ──
|
|
468
500
|
const taskStatus = statusData.status || "unknown";
|
|
469
|
-
const failedStatuses = new Set(["failed", "failed_verification", "failed_scope_violation", "failed_policy_violation", "canceled"]);
|
|
501
|
+
const failedStatuses = new Set(["failed", "failed_verification", "failed_scope_violation", "failed_policy_violation", "canceled", "timeout"]);
|
|
470
502
|
const doneStatuses = new Set(["done", "done_by_agent"]);
|
|
471
503
|
checks.push({
|
|
472
504
|
name: "task_status",
|
|
@@ -677,12 +709,8 @@ export function auditTask(taskId) {
|
|
|
677
709
|
continue;
|
|
678
710
|
}
|
|
679
711
|
// Extract npm run xxx
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
const scriptName = ref.replace(/npm\s+run\s+/i, "").replace(/[^a-zA-Z0-9:_-]/g, "");
|
|
683
|
-
if (scriptName)
|
|
684
|
-
allNpmRunRefs.add(scriptName);
|
|
685
|
-
}
|
|
712
|
+
for (const scriptName of extractNpmRunScriptNames(content))
|
|
713
|
+
allNpmRunRefs.add(scriptName);
|
|
686
714
|
// Check release claims
|
|
687
715
|
const claims = scanForReleaseClaims(content);
|
|
688
716
|
for (const c of claims)
|
|
@@ -19,6 +19,7 @@ export declare function healthCheck(catalog?: ToolCatalogSnapshot, input?: Healt
|
|
|
19
19
|
pending_reason: import("../../watcherStatus.js").PendingReason;
|
|
20
20
|
error: string | null;
|
|
21
21
|
}[];
|
|
22
|
+
last_assessment_validation_failure: import("../../assessments/assessmentDiagnostics.js").AssessmentValidationFailureDiagnostic | null;
|
|
22
23
|
} | undefined;
|
|
23
24
|
connector_visibility: {
|
|
24
25
|
status: string;
|
|
@@ -65,10 +66,21 @@ export declare function healthCheck(catalog?: ToolCatalogSnapshot, input?: Healt
|
|
|
65
66
|
tunnel: Record<string, unknown> & {
|
|
66
67
|
last_error: string | null;
|
|
67
68
|
};
|
|
69
|
+
config_path: string;
|
|
70
|
+
runtime_build_layout: string | null;
|
|
71
|
+
watcher_entry_path: string | null;
|
|
72
|
+
validator_module_path: string | null;
|
|
68
73
|
agents: import("../workspace/listAgents.js").AgentAvailability[];
|
|
69
74
|
agent_status: {
|
|
70
75
|
[k: string]: string;
|
|
71
76
|
};
|
|
77
|
+
agent_readiness: {
|
|
78
|
+
[k: string]: {
|
|
79
|
+
executable_available: boolean;
|
|
80
|
+
provider_status: "not_checked";
|
|
81
|
+
availability_scope: "executable_only";
|
|
82
|
+
};
|
|
83
|
+
};
|
|
72
84
|
search_tool_count?: 5 | undefined;
|
|
73
85
|
direct_tool_count?: number | undefined;
|
|
74
86
|
status: string;
|
|
@@ -8,6 +8,7 @@ import { PATCHWARDEN_VERSION, TOOL_SCHEMA_EPOCH } from "../../version.js";
|
|
|
8
8
|
import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, CHATGPT_SEARCH_TOOL_NAMES, resolveToolProfile } from "../catalog/toolCatalog.js";
|
|
9
9
|
import { readWatcherStatus } from "../../watcherStatus.js";
|
|
10
10
|
import { listTasks } from "../tasks/listTasks.js";
|
|
11
|
+
import { readLastAssessmentValidationFailure } from "../../assessments/assessmentDiagnostics.js";
|
|
11
12
|
const SERVER_STARTED_AT = Date.now();
|
|
12
13
|
export function healthCheck(catalog, input = {}) {
|
|
13
14
|
const config = getConfig();
|
|
@@ -120,8 +121,23 @@ export function healthCheck(catalog, input = {}) {
|
|
|
120
121
|
workspace_root: workspace,
|
|
121
122
|
tasks_dir: tasks,
|
|
122
123
|
tunnel,
|
|
124
|
+
config_path: agents.config_path,
|
|
125
|
+
runtime_build_layout: typeof watcher.supervisor?.runtime_build_layout === "string"
|
|
126
|
+
? watcher.supervisor.runtime_build_layout
|
|
127
|
+
: null,
|
|
128
|
+
watcher_entry_path: typeof watcher.supervisor?.watcher_entry_path === "string"
|
|
129
|
+
? watcher.supervisor.watcher_entry_path
|
|
130
|
+
: null,
|
|
131
|
+
validator_module_path: typeof watcher.supervisor?.validator_module_path === "string"
|
|
132
|
+
? watcher.supervisor.validator_module_path
|
|
133
|
+
: null,
|
|
123
134
|
agents: agents.agents,
|
|
124
135
|
agent_status: Object.fromEntries(agents.agents.map((agent) => [agent.name, agent.available ? "ok" : "missing"])),
|
|
136
|
+
agent_readiness: Object.fromEntries(agents.agents.map((agent) => [agent.name, {
|
|
137
|
+
executable_available: agent.available,
|
|
138
|
+
provider_status: agent.provider_status,
|
|
139
|
+
availability_scope: agent.availability_scope,
|
|
140
|
+
}])),
|
|
125
141
|
...(input.detail === "self_diagnostic" ? { self_diagnostic: buildSelfDiagnostic(config) } : {}),
|
|
126
142
|
last_error: tunnel.last_error || (!watcher.available ? watcher.reason : null) || mismatchReport[0] || null,
|
|
127
143
|
};
|
|
@@ -129,7 +145,7 @@ export function healthCheck(catalog, input = {}) {
|
|
|
129
145
|
function buildSelfDiagnostic(config) {
|
|
130
146
|
const recent = listTasks({ limit: 20 });
|
|
131
147
|
const failures = recent.tasks
|
|
132
|
-
.filter((task) => task.status.startsWith("failed") || task.status === "canceled")
|
|
148
|
+
.filter((task) => task.status.startsWith("failed") || task.status === "canceled" || task.status === "timeout")
|
|
133
149
|
.slice(0, 10)
|
|
134
150
|
.map((task) => ({
|
|
135
151
|
task_id: task.task_id,
|
|
@@ -145,6 +161,7 @@ function buildSelfDiagnostic(config) {
|
|
|
145
161
|
configured_agents: Object.keys(config.agents),
|
|
146
162
|
recent_tasks_returned: recent.tasks.length,
|
|
147
163
|
recent_failures: failures,
|
|
164
|
+
last_assessment_validation_failure: readLastAssessmentValidationFailure(),
|
|
148
165
|
};
|
|
149
166
|
const safe = redactSensitiveValue(diagnostic);
|
|
150
167
|
return {
|
|
@@ -171,6 +188,11 @@ function readWatcherSupervisorStatus() {
|
|
|
171
188
|
restart_attempts: Number.isInteger(Number(raw.restart_attempts)) ? Number(raw.restart_attempts) : 0,
|
|
172
189
|
last_error: safeText(raw.last_error, 500) || null,
|
|
173
190
|
checked_at: safeText(raw.checked_at, 80) || null,
|
|
191
|
+
project_root: safeText(raw.project_root, 500) || null,
|
|
192
|
+
config_path: safeText(raw.config_path, 500) || null,
|
|
193
|
+
watcher_entry_path: safeText(raw.watcher_entry_path, 500) || null,
|
|
194
|
+
validator_module_path: safeText(raw.validator_module_path, 500) || null,
|
|
195
|
+
runtime_build_layout: safeText(raw.runtime_build_layout, 80) || null,
|
|
174
196
|
};
|
|
175
197
|
}
|
|
176
198
|
catch {
|
|
@@ -239,6 +261,12 @@ function readTunnelStatus() {
|
|
|
239
261
|
: [],
|
|
240
262
|
tool_manifest_sha256: safeText(raw.tool_manifest_sha256, 80) || null,
|
|
241
263
|
core_tools_ready: Boolean(raw.core_tools_ready),
|
|
264
|
+
project_root: safeText(raw.project_root, 500) || null,
|
|
265
|
+
config_path: safeText(raw.config_path, 500) || null,
|
|
266
|
+
mcp_entry_path: safeText(raw.mcp_entry_path, 500) || null,
|
|
267
|
+
watcher_entry_path: safeText(raw.watcher_entry_path, 500) || null,
|
|
268
|
+
validator_module_path: safeText(raw.validator_module_path, 500) || null,
|
|
269
|
+
runtime_build_layout: safeText(raw.runtime_build_layout, 80) || null,
|
|
242
270
|
};
|
|
243
271
|
const redacted = redactSensitiveContent(JSON.stringify(allowed));
|
|
244
272
|
return JSON.parse(redacted.content);
|
|
@@ -4,6 +4,7 @@ import { getTasksDir, getConfig } from "../../config.js";
|
|
|
4
4
|
import { guardReadPath } from "../../security/pathGuard.js";
|
|
5
5
|
import { writeTaskProgress } from "../../runner/taskProgress.js";
|
|
6
6
|
import { mutateTaskStatus } from "../../runner/taskStatusStore.js";
|
|
7
|
+
import { isTerminalTaskStatus } from "./taskStates.js";
|
|
7
8
|
export function cancelTask(taskId) {
|
|
8
9
|
return requestTaskTermination(taskId, false);
|
|
9
10
|
}
|
|
@@ -17,7 +18,7 @@ export function requestTaskTermination(taskId, force) {
|
|
|
17
18
|
const now = new Date().toISOString();
|
|
18
19
|
const outcome = mutateTaskStatus(statusFile, (current) => {
|
|
19
20
|
const currentStatus = current.status;
|
|
20
|
-
if (
|
|
21
|
+
if (isTerminalTaskStatus(currentStatus)) {
|
|
21
22
|
return { result: {
|
|
22
23
|
response: {
|
|
23
24
|
task_id: taskId,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ChangePolicy, type TaskTemplateName } from "../taskTemplates.js";
|
|
2
2
|
import { type PendingReason, type WatcherStatusSnapshot } from "../../watcherStatus.js";
|
|
3
|
-
export type TaskStatus = "pending" | "running" | "collecting_artifacts" | "done_by_agent" | "accepted" | "rejected" | "needs_fix" | "blocked" | "done" | "failed" | "failed_verification" | "failed_scope_violation" | "failed_policy_violation" | "failed_stale" | "orphaned" | "canceled";
|
|
3
|
+
export type TaskStatus = "pending" | "running" | "executing_agent" | "verifying" | "collecting_artifacts" | "cancel_requested" | "done_by_agent" | "accepted" | "rejected" | "needs_fix" | "blocked" | "done" | "failed" | "failed_verification" | "failed_scope_violation" | "failed_policy_violation" | "failed_stale" | "orphaned" | "timeout" | "canceled";
|
|
4
4
|
/**
|
|
5
5
|
* v0.7.2 acceptance status — only meaningful for done_by_agent.
|
|
6
6
|
* - "pending": done_by_agent reached but not yet audited/accepted
|
|
@@ -11,7 +11,7 @@ export type TaskStatus = "pending" | "running" | "collecting_artifacts" | "done_
|
|
|
11
11
|
* - null: status has no acceptance semantics (running, failed, etc.)
|
|
12
12
|
*/
|
|
13
13
|
export type AcceptanceStatus = "pending" | "accepted" | "rejected" | "needs_fix" | "blocked" | null;
|
|
14
|
-
export type TaskPhase = "queued" | "preparing" | "executing_agent" | "running_tests" | "collecting_artifacts" | "canceling" | "terminating" | "completed" | "failed" | "failed_verification" | "failed_scope_violation" | "failed_policy_violation" | "failed_stale" | "orphaned" | "done_by_agent" | "accepted" | "rejected" | "needs_fix" | "blocked" | "canceled";
|
|
14
|
+
export type TaskPhase = "queued" | "preparing" | "executing_agent" | "running_tests" | "collecting_artifacts" | "canceling" | "terminating" | "completed" | "failed" | "failed_verification" | "failed_scope_violation" | "failed_policy_violation" | "failed_stale" | "orphaned" | "timeout" | "done_by_agent" | "accepted" | "rejected" | "needs_fix" | "blocked" | "canceled";
|
|
15
15
|
export interface CreateTaskInput {
|
|
16
16
|
plan_id?: string;
|
|
17
17
|
inline_plan?: string;
|
|
@@ -7,14 +7,16 @@ import { guardTestCommand } from "../../security/commandGuard.js";
|
|
|
7
7
|
import { guardRuntimeSelfModification } from "../../security/runtimeGuard.js";
|
|
8
8
|
import { guardPlanContent } from "../../security/planGuard.js";
|
|
9
9
|
import { assessRisk } from "../../security/riskEngine.js";
|
|
10
|
-
import { createAssessment, readAssessment, validateAssessmentFreshness, generateAssessmentId, createAssessmentDir, } from "../../assessments/assessmentStore.js";
|
|
10
|
+
import { createAssessment, readAssessment, validateAssessmentFreshness, markAssessmentUsed, generateAssessmentId, createAssessmentDir, } from "../../assessments/assessmentStore.js";
|
|
11
11
|
import { runAgentAssessment } from "../../assessments/agentAssessor.js";
|
|
12
|
+
import { recordAssessmentValidationFailure } from "../../assessments/assessmentDiagnostics.js";
|
|
13
|
+
import { ASSESSMENT_SECURITY_SNAPSHOT_VERSION } from "../../assessments/securitySnapshot.js";
|
|
12
14
|
import { captureRepoSnapshot } from "../../runner/changeCapture.js";
|
|
13
15
|
import { writeTaskProgress } from "../../runner/taskProgress.js";
|
|
14
16
|
import { PatchWardenError } from "../../errors.js";
|
|
15
17
|
import { savePlan } from "../goals/savePlan.js";
|
|
16
18
|
import { expandTaskTemplate, TASK_TEMPLATE_NAMES, } from "../taskTemplates.js";
|
|
17
|
-
import { PATCHWARDEN_VERSION, ARTIFACT_SCHEMA_VERSION } from "../../version.js";
|
|
19
|
+
import { PATCHWARDEN_VERSION, ARTIFACT_SCHEMA_VERSION, TOOL_SCHEMA_EPOCH } from "../../version.js";
|
|
18
20
|
import { getLastToolCatalogSnapshot, resolveToolProfile } from "../catalog/toolCatalog.js";
|
|
19
21
|
import { derivePendingReason, readWatcherStatus, } from "../../watcherStatus.js";
|
|
20
22
|
import { routeAgent } from "../../agents/agentRouter.js";
|
|
@@ -30,6 +32,9 @@ export async function createTask(input) {
|
|
|
30
32
|
let effectiveInput = input;
|
|
31
33
|
if (executionMode === "execute" && input.assessment_id) {
|
|
32
34
|
assessmentRecord = readAssessment(input.assessment_id);
|
|
35
|
+
if (assessmentRecord.used_at) {
|
|
36
|
+
throw new PatchWardenError("assessment_used", `Assessment "${input.assessment_id}" has already been used.`, "Run create_task with execution_mode=assess_only again to obtain a new assessment_id.", true, { assessment_id: input.assessment_id, used_at: assessmentRecord.used_at });
|
|
37
|
+
}
|
|
33
38
|
// Parameter mismatch check: if ChatGPT passes params that differ from the assessment, reject
|
|
34
39
|
effectiveInput = mergeAssessmentIntoInput(input, assessmentRecord);
|
|
35
40
|
}
|
|
@@ -247,6 +252,10 @@ export async function createTask(input) {
|
|
|
247
252
|
agent: effectiveInput.agent,
|
|
248
253
|
timeout_seconds: timeoutSeconds,
|
|
249
254
|
change_policy: changePolicy,
|
|
255
|
+
scope: effectiveInput.scope,
|
|
256
|
+
forbidden: effectiveInput.forbidden,
|
|
257
|
+
verification: effectiveInput.verification,
|
|
258
|
+
done_evidence: effectiveInput.done_evidence,
|
|
250
259
|
snapshot,
|
|
251
260
|
...(preGeneratedAssessmentId ? { assessment_id: preGeneratedAssessmentId } : {}),
|
|
252
261
|
...(preGeneratedAssessmentDir ? { assessment_dir: preGeneratedAssessmentDir } : {}),
|
|
@@ -335,8 +344,36 @@ export async function createTask(input) {
|
|
|
335
344
|
const snapshot = await captureRepoSnapshot(safeRepoPath);
|
|
336
345
|
const validation = validateAssessmentFreshness(input.assessment_id, snapshot);
|
|
337
346
|
if (!validation.valid) {
|
|
338
|
-
|
|
347
|
+
recordAssessmentValidationFailure(validation);
|
|
348
|
+
throw new PatchWardenError(validation.failure_reason || "assessment_validation_failed", `Assessment "${input.assessment_id}" is no longer valid: ${validation.failure_reason}`, "Re-run create_task with execution_mode=assess_only to get a fresh assessment_id.", true, {
|
|
349
|
+
assessment_id: input.assessment_id,
|
|
350
|
+
reason_code: validation.failure_reason,
|
|
351
|
+
failure_reason: validation.failure_reason,
|
|
352
|
+
...(validation.expected_hash ? { expected_hash: validation.expected_hash } : {}),
|
|
353
|
+
...(validation.actual_hash ? { actual_hash: validation.actual_hash } : {}),
|
|
354
|
+
...(validation.expected_hash ? { assessment_config_sha256: validation.expected_hash } : {}),
|
|
355
|
+
...(validation.actual_hash ? { current_config_sha256: validation.actual_hash } : {}),
|
|
356
|
+
snapshot_version: validation.assessment?.assessment_security_snapshot_version || null,
|
|
357
|
+
assessment_created_at: validation.assessment?.created_at || null,
|
|
358
|
+
assessment_expires_at: validation.assessment?.expires_at || null,
|
|
359
|
+
config_changed: validation.failure_reason === "assessment_stale_config",
|
|
360
|
+
...(validation.config_change_categories
|
|
361
|
+
? {
|
|
362
|
+
config_change_categories: validation.config_change_categories,
|
|
363
|
+
changed_field_names: validation.config_change_categories,
|
|
364
|
+
changed_config_sections: validation.config_change_categories,
|
|
365
|
+
}
|
|
366
|
+
: {}),
|
|
367
|
+
assessment_fingerprint_version: validation.assessment?.assessment_security_snapshot_version || null,
|
|
368
|
+
current_fingerprint_version: ASSESSMENT_SECURITY_SNAPSHOT_VERSION,
|
|
369
|
+
assessment_schema_epoch: validation.assessment?.assessment_schema_epoch || null,
|
|
370
|
+
current_schema_epoch: TOOL_SCHEMA_EPOCH,
|
|
371
|
+
validator_module_path: "dist/assessments/assessmentStore.js",
|
|
372
|
+
validator_build_id: `patchwarden-${PATCHWARDEN_VERSION}:${TOOL_SCHEMA_EPOCH}:${ASSESSMENT_SECURITY_SNAPSHOT_VERSION}`,
|
|
373
|
+
watcher_instance_id: process.env.PATCHWARDEN_WATCHER_INSTANCE_ID || null,
|
|
374
|
+
});
|
|
339
375
|
}
|
|
376
|
+
markAssessmentUsed(input.assessment_id);
|
|
340
377
|
}
|
|
341
378
|
const { taskId, taskDir } = createTaskDirectory(tasksDir, config.workspaceRoot, config.tasksDir);
|
|
342
379
|
const status = "pending";
|
|
@@ -435,26 +472,50 @@ function mergeAssessmentIntoInput(input, record) {
|
|
|
435
472
|
// Do NOT set template/inline_plan alongside plan_id — createTask requires exactly one plan source.
|
|
436
473
|
const merged = {
|
|
437
474
|
...input,
|
|
438
|
-
plan_id: record.plan_id ||
|
|
475
|
+
plan_id: record.plan_id || undefined,
|
|
439
476
|
template: undefined,
|
|
440
477
|
inline_plan: undefined,
|
|
441
|
-
goal: record.goal ||
|
|
478
|
+
goal: record.goal || undefined,
|
|
442
479
|
agent: record.agent,
|
|
443
480
|
repo_path: record.repo_path,
|
|
444
481
|
test_command: record.test_command || undefined,
|
|
445
|
-
verify_commands: record.verify_commands ||
|
|
446
|
-
timeout_seconds: record.timeout_seconds
|
|
482
|
+
verify_commands: record.verify_commands || [],
|
|
483
|
+
timeout_seconds: record.timeout_seconds,
|
|
484
|
+
scope: record.scope || [],
|
|
485
|
+
forbidden: record.forbidden || [],
|
|
486
|
+
verification: record.verification || [],
|
|
487
|
+
done_evidence: record.done_evidence || [],
|
|
447
488
|
};
|
|
448
489
|
// Parameter mismatch check: if caller passed explicit params that differ from assessment
|
|
449
|
-
if (input.template &&
|
|
490
|
+
if (input.template && input.template !== record.template) {
|
|
450
491
|
throw new PatchWardenError("assessment_parameter_mismatch", `template mismatch: caller passed "${input.template}" but assessment has "${record.template}".`, "Do not override assessment-locked parameters. Use the same assessment_id as-is.", true, { field: "template", assessment_value: record.template, caller_value: input.template });
|
|
451
492
|
}
|
|
452
|
-
if (input.goal &&
|
|
493
|
+
if (input.goal && input.goal !== record.goal) {
|
|
453
494
|
throw new PatchWardenError("assessment_parameter_mismatch", `goal mismatch: caller passed a different goal than the assessment.`, "Do not override assessment-locked parameters. Use the same assessment_id as-is.", true, { field: "goal" });
|
|
454
495
|
}
|
|
455
496
|
if (input.repo_path && record.repo_path && input.repo_path !== record.repo_path) {
|
|
456
497
|
throw new PatchWardenError("assessment_parameter_mismatch", `repo_path mismatch: caller passed "${input.repo_path}" but assessment has "${record.repo_path}".`, "Do not override assessment-locked parameters. Use the same assessment_id as-is.", true, { field: "repo_path", assessment_value: record.repo_path, caller_value: input.repo_path });
|
|
457
498
|
}
|
|
499
|
+
if (input.agent && input.agent !== record.agent) {
|
|
500
|
+
throw new PatchWardenError("assessment_parameter_mismatch", `agent mismatch: caller passed "${input.agent}" but assessment has "${record.agent}".`, "Do not override assessment-locked parameters. Use the same assessment_id as-is.", true, { field: "agent" });
|
|
501
|
+
}
|
|
502
|
+
if (input.verify_commands
|
|
503
|
+
&& JSON.stringify(input.verify_commands) !== JSON.stringify(record.verify_commands || [])) {
|
|
504
|
+
throw new PatchWardenError("assessment_parameter_mismatch", "verify_commands mismatch: caller passed commands different from the assessment.", "Do not override assessment-locked parameters. Use the same assessment_id as-is.", true, { field: "verify_commands" });
|
|
505
|
+
}
|
|
506
|
+
if (input.test_command && input.test_command !== record.test_command) {
|
|
507
|
+
throw new PatchWardenError("assessment_parameter_mismatch", "test_command mismatch: caller passed a command different from the assessment.", "Do not override assessment-locked parameters. Use the same assessment_id as-is.", true, { field: "test_command" });
|
|
508
|
+
}
|
|
509
|
+
if (input.timeout_seconds !== undefined && input.timeout_seconds !== record.timeout_seconds) {
|
|
510
|
+
throw new PatchWardenError("assessment_parameter_mismatch", "timeout_seconds mismatch: caller passed a timeout different from the assessment.", "Do not override assessment-locked parameters. Use the same assessment_id as-is.", true, { field: "timeout_seconds" });
|
|
511
|
+
}
|
|
512
|
+
for (const field of ["scope", "forbidden", "verification", "done_evidence"]) {
|
|
513
|
+
const callerValue = input[field];
|
|
514
|
+
const assessmentValue = record[field] || [];
|
|
515
|
+
if (callerValue && JSON.stringify(callerValue) !== JSON.stringify(assessmentValue)) {
|
|
516
|
+
throw new PatchWardenError("assessment_parameter_mismatch", `${field} mismatch: caller passed values different from the assessment.`, "Do not override assessment-locked parameters. Use the same assessment_id as-is.", true, { field });
|
|
517
|
+
}
|
|
518
|
+
}
|
|
458
519
|
return merged;
|
|
459
520
|
}
|
|
460
521
|
function createTaskDirectory(tasksDir, workspaceRoot, configuredTasksDir) {
|
|
@@ -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
|
// ── Thresholds (seconds) ──────────────────────────────────────────
|
|
11
12
|
//
|
|
12
13
|
// These are intentionally conservative. A task is only "stale" when multiple
|
|
@@ -81,16 +82,11 @@ export function diagnoseTask(input, config = getConfig()) {
|
|
|
81
82
|
const statusStr = typeof status.status === "string" ? status.status : "unknown";
|
|
82
83
|
const phaseStr = (runtime.phase || (typeof status.phase === "string" ? status.phase : "queued"));
|
|
83
84
|
// ── Terminal tasks: no diagnosis needed ──
|
|
84
|
-
|
|
85
|
-
"done", "done_by_agent", "failed", "failed_verification",
|
|
86
|
-
"failed_scope_violation", "failed_policy_violation",
|
|
87
|
-
"failed_stale", "orphaned", "canceled",
|
|
88
|
-
]);
|
|
89
|
-
if (terminalStatuses.has(statusStr)) {
|
|
85
|
+
if (isTerminalTaskStatus(statusStr)) {
|
|
90
86
|
return buildTerminalDiagnosis(taskId, statusStr, phaseStr);
|
|
91
87
|
}
|
|
92
|
-
//
|
|
93
|
-
if (statusStr
|
|
88
|
+
// Diagnose both current status spellings and legacy phase-as-status records.
|
|
89
|
+
if (!isActiveTaskStatus(statusStr)) {
|
|
94
90
|
return buildUnknownDiagnosis(taskId, statusStr, phaseStr, [`unexpected status "${statusStr}" for diagnosis`]);
|
|
95
91
|
}
|
|
96
92
|
// ── Gather evidence ──
|
|
@@ -5,6 +5,7 @@ import { guardReadPath } from "../../security/pathGuard.js";
|
|
|
5
5
|
import { guardSensitivePath } from "../../security/sensitiveGuard.js";
|
|
6
6
|
import { readTaskRuntime } from "../../runner/taskRuntime.js";
|
|
7
7
|
import { derivePendingReason, readWatcherStatus, } from "../../watcherStatus.js";
|
|
8
|
+
import { isTerminalTaskStatus } from "./taskStates.js";
|
|
8
9
|
export function getTaskStatus(taskId) {
|
|
9
10
|
const config = getConfig();
|
|
10
11
|
const tasksDir = getTasksDir(config);
|
|
@@ -18,7 +19,11 @@ export function getTaskStatus(taskId) {
|
|
|
18
19
|
const raw = readFileSync(statusFile, "utf-8");
|
|
19
20
|
const status = JSON.parse(raw);
|
|
20
21
|
const runtime = readTaskRuntime(taskDir);
|
|
21
|
-
|
|
22
|
+
// A terminal status is authoritative. A stale runtime phase from a crashed
|
|
23
|
+
// Runner must never make a reconciled task look active again.
|
|
24
|
+
const phase = isTerminalTaskStatus(String(status.status))
|
|
25
|
+
? status.phase || "queued"
|
|
26
|
+
: runtime.phase || status.phase || "queued";
|
|
22
27
|
const watcher = readWatcherStatus(config);
|
|
23
28
|
const pendingReason = derivePendingReason({ status: status.status, phase }, watcher);
|
|
24
29
|
return {
|
|
@@ -5,15 +5,7 @@ import { guardReadPath } from "../../security/pathGuard.js";
|
|
|
5
5
|
import { getTaskStatus } from "./getTaskStatus.js";
|
|
6
6
|
import { redactSensitiveValue } from "../../security/contentRedaction.js";
|
|
7
7
|
import { readTextFilePrefixSync, readTextFileTailLinesSync, } from "../../utils/boundedFile.js";
|
|
8
|
-
|
|
9
|
-
"done",
|
|
10
|
-
"done_by_agent",
|
|
11
|
-
"failed",
|
|
12
|
-
"failed_verification",
|
|
13
|
-
"failed_scope_violation",
|
|
14
|
-
"failed_policy_violation",
|
|
15
|
-
"canceled",
|
|
16
|
-
]);
|
|
8
|
+
import { isTerminalTaskStatus } from "./taskStates.js";
|
|
17
9
|
export function getTaskSummary(taskId, options = {}) {
|
|
18
10
|
const config = getConfig();
|
|
19
11
|
const taskDir = resolve(getTasksDir(config), taskId);
|
|
@@ -24,7 +16,7 @@ export function getTaskSummary(taskId, options = {}) {
|
|
|
24
16
|
const verifyRead = tryReadJson(join(taskDir, "verify.json"));
|
|
25
17
|
const result = resultRead.data;
|
|
26
18
|
const verify = verifyRead.data;
|
|
27
|
-
const terminal =
|
|
19
|
+
const terminal = isTerminalTaskStatus(String(status.status));
|
|
28
20
|
// Phase 4: Use new_out_of_scope_changes (task-caused) for acceptance status.
|
|
29
21
|
// Pre-existing external dirty files that didn't change should NOT fail acceptance.
|
|
30
22
|
const outOfScope = asArray(result.new_out_of_scope_changes
|
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { getTasksDir, getPlansDir, getConfig } from "../../config.js";
|
|
4
4
|
import { readTaskRuntime } from "../../runner/taskRuntime.js";
|
|
5
5
|
import { derivePendingReason, readWatcherStatus, } from "../../watcherStatus.js";
|
|
6
|
+
import { isActiveTaskStatus } from "./taskStates.js";
|
|
6
7
|
export function listTasks(input) {
|
|
7
8
|
const config = getConfig();
|
|
8
9
|
const tasksDir = getTasksDir(config);
|
|
@@ -48,7 +49,7 @@ export function listTasks(input) {
|
|
|
48
49
|
if (taskAcceptance !== filterAcceptance)
|
|
49
50
|
continue;
|
|
50
51
|
}
|
|
51
|
-
if (input?.active_only && !
|
|
52
|
+
if (input?.active_only && !isActiveTaskStatus(String(data.status)))
|
|
52
53
|
continue;
|
|
53
54
|
const normalizedRepo = String(data.repo_path || ".").replace(/\\/g, "/");
|
|
54
55
|
const normalizedResolvedRepo = String(data.resolved_repo_path || "").replace(/\\/g, "/");
|