patchwarden 0.6.1 → 1.1.0
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/PatchWarden-Control-Tray.cmd +11 -0
- package/PatchWarden-Control.cmd +6 -0
- package/PatchWarden-Desktop.cmd +5 -0
- package/PatchWarden.cmd +1 -1
- package/README.en.md +112 -24
- package/README.md +36 -25
- package/Restart-PatchWarden-Control.cmd +6 -0
- package/Stop-PatchWarden.cmd +11 -0
- package/dist/agents/agentRouter.d.ts +40 -0
- package/dist/agents/agentRouter.js +95 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +6 -2
- package/dist/controlCenter.d.ts +14 -0
- package/dist/controlCenter.js +2015 -0
- package/dist/doctor.js +35 -4
- package/dist/goal/acceptanceEngine.d.ts +62 -0
- package/dist/goal/acceptanceEngine.js +103 -0
- package/dist/goal/acceptanceTemplate.d.ts +10 -0
- package/dist/goal/acceptanceTemplate.js +104 -0
- package/dist/goal/goalGraph.d.ts +58 -0
- package/dist/goal/goalGraph.js +189 -0
- package/dist/goal/goalProgress.d.ts +81 -0
- package/dist/goal/goalProgress.js +216 -0
- package/dist/goal/goalStatus.d.ts +60 -0
- package/dist/goal/goalStatus.js +137 -0
- package/dist/goal/goalStore.d.ts +79 -0
- package/dist/goal/goalStore.js +211 -0
- package/dist/goal/handoffExport.d.ts +34 -0
- package/dist/goal/handoffExport.js +183 -0
- package/dist/goal/subgoalSync.d.ts +40 -0
- package/dist/goal/subgoalSync.js +72 -0
- package/dist/goal/worktreeManager.d.ts +96 -0
- package/dist/goal/worktreeManager.js +292 -0
- package/dist/logging.d.ts +44 -0
- package/dist/logging.js +68 -0
- package/dist/release/releaseGate.d.ts +99 -0
- package/dist/release/releaseGate.js +475 -0
- package/dist/runner/postTaskCleanup.d.ts +13 -0
- package/dist/runner/postTaskCleanup.js +147 -0
- package/dist/runner/runTask.js +50 -10
- package/dist/security/discoveryTokenStore.d.ts +62 -0
- package/dist/security/discoveryTokenStore.js +100 -0
- package/dist/security/toolInvocationGuard.d.ts +35 -0
- package/dist/security/toolInvocationGuard.js +153 -0
- package/dist/smoke-test.js +18 -10
- package/dist/taskRuntime.d.ts +17 -0
- package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
- package/dist/test/unit/acceptance-engine.test.js +228 -0
- package/dist/test/unit/agent-router.test.d.ts +1 -0
- package/dist/test/unit/agent-router.test.js +287 -0
- package/dist/test/unit/audit-checks.test.d.ts +1 -0
- package/dist/test/unit/audit-checks.test.js +350 -0
- package/dist/test/unit/diagnose-task.test.d.ts +1 -0
- package/dist/test/unit/diagnose-task.test.js +457 -0
- package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
- package/dist/test/unit/discovery-token-store.test.js +139 -0
- package/dist/test/unit/goal-graph.test.d.ts +1 -0
- package/dist/test/unit/goal-graph.test.js +298 -0
- package/dist/test/unit/goal-progress.test.d.ts +1 -0
- package/dist/test/unit/goal-progress.test.js +381 -0
- package/dist/test/unit/goal-status.test.d.ts +1 -0
- package/dist/test/unit/goal-status.test.js +215 -0
- package/dist/test/unit/goal-store.test.d.ts +1 -0
- package/dist/test/unit/goal-store.test.js +253 -0
- package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
- package/dist/test/unit/goal-subgoal-task.test.js +55 -0
- package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
- package/dist/test/unit/goal-tools-registry.test.js +190 -0
- package/dist/test/unit/handoff-export.test.d.ts +1 -0
- package/dist/test/unit/handoff-export.test.js +263 -0
- package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
- package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
- package/dist/test/unit/logging.test.js +127 -5
- package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
- package/dist/test/unit/post-task-cleanup.test.js +48 -0
- package/dist/test/unit/reconcile-tasks.test.d.ts +1 -0
- package/dist/test/unit/reconcile-tasks.test.js +456 -0
- package/dist/test/unit/release-gate.test.d.ts +1 -0
- package/dist/test/unit/release-gate.test.js +242 -0
- package/dist/test/unit/safe-views.test.d.ts +1 -0
- package/dist/test/unit/safe-views.test.js +171 -0
- package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
- package/dist/test/unit/schema-drift-check.test.js +175 -0
- package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
- package/dist/test/unit/subgoal-sync.test.js +183 -0
- package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
- package/dist/test/unit/tool-invocation-guard.test.js +432 -0
- package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
- package/dist/test/unit/tool-usage-stats.test.js +300 -0
- package/dist/test/unit/toolSearch.test.d.ts +1 -0
- package/dist/test/unit/toolSearch.test.js +571 -0
- package/dist/test/unit/worktree-manager.test.d.ts +1 -0
- package/dist/test/unit/worktree-manager.test.js +176 -0
- package/dist/tools/auditTask.d.ts +103 -1
- package/dist/tools/auditTask.js +461 -8
- package/dist/tools/cancelTask.js +1 -1
- package/dist/tools/checkReleaseGate.d.ts +21 -0
- package/dist/tools/checkReleaseGate.js +22 -0
- package/dist/tools/createTask.d.ts +18 -2
- package/dist/tools/createTask.js +29 -1
- package/dist/tools/diagnoseTask.d.ts +45 -0
- package/dist/tools/diagnoseTask.js +347 -0
- package/dist/tools/discardWorktree.d.ts +24 -0
- package/dist/tools/discardWorktree.js +27 -0
- package/dist/tools/discoverTools.d.ts +11 -0
- package/dist/tools/discoverTools.js +39 -0
- package/dist/tools/explainTool.d.ts +11 -0
- package/dist/tools/explainTool.js +21 -0
- package/dist/tools/getTaskSummary.js +2 -1
- package/dist/tools/goalSubgoalTask.d.ts +51 -0
- package/dist/tools/goalSubgoalTask.js +110 -0
- package/dist/tools/healthCheck.d.ts +1 -0
- package/dist/tools/healthCheck.js +5 -1
- package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
- package/dist/tools/invokeDiscoveredTool.js +112 -0
- package/dist/tools/listTasks.d.ts +3 -1
- package/dist/tools/listTasks.js +14 -1
- package/dist/tools/mergeWorktree.d.ts +24 -0
- package/dist/tools/mergeWorktree.js +27 -0
- package/dist/tools/reconcileTasks.d.ts +41 -0
- package/dist/tools/reconcileTasks.js +352 -0
- package/dist/tools/registry.js +550 -0
- package/dist/tools/safeStatus.d.ts +31 -1
- package/dist/tools/safeStatus.js +43 -1
- package/dist/tools/safeViews.d.ts +256 -0
- package/dist/tools/safeViews.js +250 -0
- package/dist/tools/schemaDriftCheck.d.ts +46 -0
- package/dist/tools/schemaDriftCheck.js +80 -0
- package/dist/tools/toolCatalog.d.ts +4 -3
- package/dist/tools/toolCatalog.js +33 -11
- package/dist/tools/toolRegistry.d.ts +61 -0
- package/dist/tools/toolRegistry.js +724 -0
- package/dist/tools/toolSearch.d.ts +110 -0
- package/dist/tools/toolSearch.js +331 -0
- package/dist/tools/toolUsageStats.d.ts +41 -0
- package/dist/tools/toolUsageStats.js +116 -0
- package/dist/tools/waitForTask.js +1 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watcherStatus.d.ts +29 -0
- package/dist/watcherStatus.js +92 -1
- package/docs/control-center/README.md +33 -0
- package/docs/control-center/control-center-daily-driver.md +211 -0
- package/docs/control-center/control-center-mvp.md +205 -0
- package/docs/control-center/control-center-phase2.md +159 -0
- package/docs/demo.md +3 -0
- package/docs/release-v0.6.4.md +45 -0
- package/examples/openai-tunnel/README.md +5 -5
- package/examples/openai-tunnel/tunnel-client.example.yaml +3 -3
- package/package.json +25 -16
- package/scripts/README.md +47 -0
- package/scripts/{brand-check.js → checks/brand-check.js} +2 -2
- package/scripts/checks/control-center-smoke.js +1098 -0
- package/scripts/{control-smoke.js → checks/control-smoke.js} +19 -5
- package/scripts/{doctor-smoke.js → checks/doctor-smoke.js} +2 -1
- package/scripts/{http-mcp-smoke.js → checks/http-mcp-smoke.js} +2 -2
- package/scripts/{lifecycle-smoke.js → checks/lifecycle-smoke.js} +25 -21
- package/scripts/{mcp-manifest-check.js → checks/mcp-manifest-check.js} +54 -7
- package/scripts/{mcp-smoke.js → checks/mcp-smoke.js} +40 -11
- package/scripts/{package-manifest-check.js → checks/package-manifest-check.js} +2 -1
- package/scripts/{tunnel-supervisor-smoke.js → checks/tunnel-supervisor-smoke.js} +2 -2
- package/scripts/{unit-tests.js → checks/unit-tests.js} +1 -1
- package/scripts/{watcher-supervisor-smoke.js → checks/watcher-supervisor-smoke.js} +13 -7
- package/scripts/control/control-center-tray.ps1 +281 -0
- package/scripts/{get-patchwarden-health.ps1 → control/get-patchwarden-health.ps1} +3 -3
- package/scripts/{manage-patchwarden.ps1 → control/manage-patchwarden.ps1} +30 -4
- package/scripts/control/restart-control-center.ps1 +173 -0
- package/scripts/{restart-patchwarden.ps1 → control/restart-patchwarden.ps1} +1 -1
- package/scripts/control/start-control-center.ps1 +263 -0
- package/scripts/{start-patchwarden-tunnel.ps1 → control/start-patchwarden-tunnel.ps1} +48 -6
- package/scripts/control/stop-patchwarden.ps1 +114 -0
- package/scripts/launchers/Check-PatchWarden-Health.cmd +1 -1
- package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +1 -1
- package/scripts/launchers/Restart-PatchWarden.cmd +1 -1
- package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +1 -1
- package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +1 -1
- package/scripts/{patchwarden-mcp-direct.cmd → mcp/patchwarden-mcp-direct.cmd} +1 -1
- package/scripts/{patchwarden-mcp-stdio.cmd → mcp/patchwarden-mcp-stdio.cmd} +1 -1
- package/scripts/{pack-clean.js → release/pack-clean.js} +9 -1
- package/src/agents/agentRouter.ts +149 -0
- package/src/config.ts +9 -3
- package/src/controlCenter.ts +2166 -0
- package/src/doctor.ts +40 -5
- package/src/goal/acceptanceEngine.ts +160 -0
- package/src/goal/acceptanceTemplate.ts +121 -0
- package/src/goal/goalGraph.ts +225 -0
- package/src/goal/goalProgress.ts +301 -0
- package/src/goal/goalStatus.ts +234 -0
- package/src/goal/goalStore.ts +306 -0
- package/src/goal/handoffExport.ts +211 -0
- package/src/goal/subgoalSync.ts +82 -0
- package/src/goal/worktreeManager.ts +404 -0
- package/src/logging.ts +91 -0
- package/src/release/releaseGate.ts +567 -0
- package/src/runner/postTaskCleanup.ts +154 -0
- package/src/runner/runTask.ts +49 -10
- package/src/security/discoveryTokenStore.ts +157 -0
- package/src/security/toolInvocationGuard.ts +251 -0
- package/src/smoke-test.ts +15 -7
- package/src/taskRuntime.ts +17 -0
- package/src/test/unit/acceptance-engine.test.ts +261 -0
- package/src/test/unit/agent-router.test.ts +342 -0
- package/src/test/unit/audit-checks.test.ts +567 -0
- package/src/test/unit/diagnose-task.test.ts +544 -0
- package/src/test/unit/discovery-token-store.test.ts +181 -0
- package/src/test/unit/goal-graph.test.ts +347 -0
- package/src/test/unit/goal-progress.test.ts +538 -0
- package/src/test/unit/goal-status.test.ts +270 -0
- package/src/test/unit/goal-store.test.ts +318 -0
- package/src/test/unit/goal-subgoal-task.test.ts +72 -0
- package/src/test/unit/goal-tools-registry.test.ts +243 -0
- package/src/test/unit/handoff-export.test.ts +295 -0
- package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
- package/src/test/unit/logging.test.ts +177 -5
- package/src/test/unit/post-task-cleanup.test.ts +53 -0
- package/src/test/unit/reconcile-tasks.test.ts +551 -0
- package/src/test/unit/release-gate.test.ts +314 -0
- package/src/test/unit/safe-views.test.ts +184 -0
- package/src/test/unit/schema-drift-check.test.ts +258 -0
- package/src/test/unit/subgoal-sync.test.ts +236 -0
- package/src/test/unit/tool-invocation-guard.test.ts +542 -0
- package/src/test/unit/tool-usage-stats.test.ts +384 -0
- package/src/test/unit/toolSearch.test.ts +756 -0
- package/src/test/unit/worktree-manager.test.ts +247 -0
- package/src/tools/auditTask.ts +831 -402
- package/src/tools/cancelTask.ts +1 -1
- package/src/tools/checkReleaseGate.ts +45 -0
- package/src/tools/createTask.ts +64 -6
- package/src/tools/diagnoseTask.ts +460 -0
- package/src/tools/discardWorktree.ts +42 -0
- package/src/tools/discoverTools.ts +51 -0
- package/src/tools/explainTool.ts +26 -0
- package/src/tools/getTaskSummary.ts +2 -1
- package/src/tools/goalSubgoalTask.ts +170 -0
- package/src/tools/healthCheck.ts +4 -1
- package/src/tools/invokeDiscoveredTool.ts +163 -0
- package/src/tools/listTasks.ts +16 -2
- package/src/tools/mergeWorktree.ts +42 -0
- package/src/tools/reconcileTasks.ts +464 -0
- package/src/tools/registry.ts +618 -1
- package/src/tools/safeStatus.ts +73 -2
- package/src/tools/safeViews.ts +271 -0
- package/src/tools/schemaDriftCheck.ts +120 -0
- package/src/tools/toolCatalog.ts +34 -11
- package/src/tools/toolRegistry.ts +786 -0
- package/src/tools/toolSearch.ts +464 -0
- package/src/tools/toolUsageStats.ts +151 -0
- package/src/tools/waitForTask.ts +1 -0
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +97 -1
- package/ui/colors_and_type.css +141 -0
- package/ui/pages/audit.html +743 -0
- package/ui/pages/dashboard.html +1154 -0
- package/ui/pages/direct-sessions.html +652 -0
- package/ui/pages/logs.html +502 -0
- package/ui/pages/task-detail.html +1229 -0
- package/ui/pages/tasks.html +702 -0
- package/ui/pages/workspace.html +947 -0
- package/ui/partials/project-shell.html +362 -0
- package/ui/vendor/lucide.js +12 -0
- package/ui/vendor/tailwindcss-browser.js +947 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type PatchWardenConfig } from "../config.js";
|
|
2
|
+
export type DiagnosisType = "active_running" | "stale_running" | "possibly_stale_running" | "orphaned_running" | "artifact_collection_stuck" | "done_candidate" | "unknown" | "terminal";
|
|
3
|
+
export type DiagnosisConfidence = "high" | "medium" | "low";
|
|
4
|
+
export type SafeAction = "leave_unchanged" | "mark_failed_stale" | "mark_orphaned" | "mark_done_by_agent" | "collect_artifacts" | "recollect_artifacts" | "needs_fix";
|
|
5
|
+
export interface DiagnosisEvidence {
|
|
6
|
+
heartbeat_age_seconds: number | null;
|
|
7
|
+
stdout_age_seconds: number | null;
|
|
8
|
+
stderr_age_seconds: number | null;
|
|
9
|
+
stdout_size_bytes: number | null;
|
|
10
|
+
stderr_size_bytes: number | null;
|
|
11
|
+
child_pid: number | null;
|
|
12
|
+
child_pid_alive: boolean | null;
|
|
13
|
+
child_started_at: string | null;
|
|
14
|
+
pid_reuse_suspected: boolean;
|
|
15
|
+
watcher_instance_id: string | null;
|
|
16
|
+
current_watcher_instance_id: string | null;
|
|
17
|
+
watcher_owns_task: boolean;
|
|
18
|
+
watcher_ownership_reason: string;
|
|
19
|
+
has_result_md: boolean;
|
|
20
|
+
has_test_log: boolean;
|
|
21
|
+
has_git_diff: boolean;
|
|
22
|
+
has_artifact_manifest: boolean;
|
|
23
|
+
task_started_at: string | null;
|
|
24
|
+
status_updated_at: string | null;
|
|
25
|
+
collecting_artifacts_phase: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface DiagnoseTaskOutput {
|
|
28
|
+
task_id: string;
|
|
29
|
+
status: string;
|
|
30
|
+
phase: string | null;
|
|
31
|
+
diagnosis: DiagnosisType;
|
|
32
|
+
confidence: DiagnosisConfidence;
|
|
33
|
+
reasons: string[];
|
|
34
|
+
safe_actions: SafeAction[];
|
|
35
|
+
evidence: DiagnosisEvidence;
|
|
36
|
+
logs: {
|
|
37
|
+
stdout_tail: string | null;
|
|
38
|
+
stderr_tail: string | null;
|
|
39
|
+
} | null;
|
|
40
|
+
}
|
|
41
|
+
export interface DiagnoseTaskInput {
|
|
42
|
+
task_id: string;
|
|
43
|
+
include_logs?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function diagnoseTask(input: DiagnoseTaskInput, config?: PatchWardenConfig): DiagnoseTaskOutput;
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { getTasksDir, getConfig } from "../config.js";
|
|
4
|
+
import { guardReadPath } from "../security/pathGuard.js";
|
|
5
|
+
import { guardSensitivePath } from "../security/sensitiveGuard.js";
|
|
6
|
+
import { redactSensitiveContent } from "../security/contentRedaction.js";
|
|
7
|
+
import { readTaskRuntime } from "../taskRuntime.js";
|
|
8
|
+
import { isWatcherOwningTask, readWatcherInstanceId } from "../watcherStatus.js";
|
|
9
|
+
// ── Thresholds (seconds) ──────────────────────────────────────────
|
|
10
|
+
//
|
|
11
|
+
// These are intentionally conservative. A task is only "stale" when multiple
|
|
12
|
+
// signals agree. A single stale signal produces "possibly_stale_running" or
|
|
13
|
+
// "unknown", never an automatic safe_fix.
|
|
14
|
+
const HEARTBEAT_STALE_SECONDS = 300; // 5 min: heartbeat this old is a strong stale signal
|
|
15
|
+
const HEARTBEAT_POSSIBLY_STALE_SECONDS = 120; // 2 min: heartbeat this old is a weak stale signal
|
|
16
|
+
const LOG_STALE_SECONDS = 300; // 5 min: stdout/stderr unchanged this long is stale
|
|
17
|
+
const LOG_TAIL_LINES = 40;
|
|
18
|
+
// ── PID liveness check ─────────────────────────────────────────────
|
|
19
|
+
/**
|
|
20
|
+
* Check whether a PID is alive without sending a signal that could disturb it.
|
|
21
|
+
* Returns null when the check itself fails (e.g., permission denied),
|
|
22
|
+
* so callers can avoid treating "couldn't check" as "process is dead".
|
|
23
|
+
*/
|
|
24
|
+
function isPidAlive(pid) {
|
|
25
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
26
|
+
return false;
|
|
27
|
+
try {
|
|
28
|
+
// process.kill(pid, 0) does not actually kill; it just checks liveness.
|
|
29
|
+
// It throws if the process does not exist or the caller lacks permission.
|
|
30
|
+
process.kill(pid, 0);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const code = err.code;
|
|
35
|
+
if (code === "ESRCH")
|
|
36
|
+
return false; // No such process
|
|
37
|
+
if (code === "EPERM")
|
|
38
|
+
return null; // Alive but not ours — treat as unknown
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// ── File age helper ────────────────────────────────────────────────
|
|
43
|
+
function fileAgeSeconds(filePath, nowMs) {
|
|
44
|
+
if (!existsSync(filePath))
|
|
45
|
+
return null;
|
|
46
|
+
try {
|
|
47
|
+
const stat = statSync(filePath);
|
|
48
|
+
return { age: Math.max(0, Math.round((nowMs - stat.mtimeMs) / 1000)), size: stat.size };
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function readTailLines(filePath, lines) {
|
|
55
|
+
if (!existsSync(filePath))
|
|
56
|
+
return null;
|
|
57
|
+
try {
|
|
58
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
59
|
+
const tail = raw.split("\n").slice(-lines).join("\n");
|
|
60
|
+
return redactSensitiveContent(tail).content;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// ── Main diagnosis entry point ─────────────────────────────────────
|
|
67
|
+
export function diagnoseTask(input, config = getConfig()) {
|
|
68
|
+
const taskId = input.task_id;
|
|
69
|
+
const tasksDir = getTasksDir(config);
|
|
70
|
+
const taskDir = resolve(tasksDir, taskId);
|
|
71
|
+
const statusFile = join(taskDir, "status.json");
|
|
72
|
+
guardReadPath(statusFile, config.workspaceRoot, config.tasksDir);
|
|
73
|
+
guardSensitivePath(statusFile);
|
|
74
|
+
if (!existsSync(statusFile)) {
|
|
75
|
+
throw new Error(`Task not found: "${taskId}". Check the task ID or call list_tasks.`);
|
|
76
|
+
}
|
|
77
|
+
const raw = readFileSync(statusFile, "utf-8");
|
|
78
|
+
const status = JSON.parse(raw);
|
|
79
|
+
const runtime = readTaskRuntime(taskDir);
|
|
80
|
+
const nowMs = Date.now();
|
|
81
|
+
const statusStr = typeof status.status === "string" ? status.status : "unknown";
|
|
82
|
+
const phaseStr = (runtime.phase || (typeof status.phase === "string" ? status.phase : "queued"));
|
|
83
|
+
// ── Terminal tasks: no diagnosis needed ──
|
|
84
|
+
const terminalStatuses = new Set([
|
|
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)) {
|
|
90
|
+
return buildTerminalDiagnosis(taskId, statusStr, phaseStr);
|
|
91
|
+
}
|
|
92
|
+
// ── Only diagnose running / collecting_artifacts / pending ──
|
|
93
|
+
if (statusStr !== "running" && statusStr !== "collecting_artifacts" && statusStr !== "pending") {
|
|
94
|
+
return buildUnknownDiagnosis(taskId, statusStr, phaseStr, [`unexpected status "${statusStr}" for diagnosis`]);
|
|
95
|
+
}
|
|
96
|
+
// ── Gather evidence ──
|
|
97
|
+
const heartbeatStr = (typeof runtime.last_heartbeat_at === "string" && runtime.last_heartbeat_at) ||
|
|
98
|
+
(typeof status.last_heartbeat_at === "string" && status.last_heartbeat_at) ||
|
|
99
|
+
(typeof status.updated_at === "string" && status.updated_at) ||
|
|
100
|
+
null;
|
|
101
|
+
const heartbeatMs = heartbeatStr ? Date.parse(heartbeatStr) : NaN;
|
|
102
|
+
const heartbeatAgeSeconds = Number.isFinite(heartbeatMs)
|
|
103
|
+
? Math.max(0, Math.round((nowMs - heartbeatMs) / 1000))
|
|
104
|
+
: null;
|
|
105
|
+
const stdoutPath = join(taskDir, "stdout.log");
|
|
106
|
+
const stderrPath = join(taskDir, "stderr.log");
|
|
107
|
+
const stdoutStat = fileAgeSeconds(stdoutPath, nowMs);
|
|
108
|
+
const stderrStat = fileAgeSeconds(stderrPath, nowMs);
|
|
109
|
+
const resultMdPath = join(taskDir, "result.md");
|
|
110
|
+
const testLogPath = join(taskDir, "test.log");
|
|
111
|
+
const gitDiffPath = join(taskDir, "git.diff");
|
|
112
|
+
const artifactManifestPath = join(taskDir, "artifact_manifest.json");
|
|
113
|
+
const hasResultMd = existsSync(resultMdPath);
|
|
114
|
+
const hasTestLog = existsSync(testLogPath);
|
|
115
|
+
const hasGitDiff = existsSync(gitDiffPath);
|
|
116
|
+
const hasArtifactManifest = existsSync(artifactManifestPath);
|
|
117
|
+
const childPid = typeof runtime.child_pid === "number" ? runtime.child_pid : null;
|
|
118
|
+
const childStartedAt = typeof runtime.child_started_at === "string" ? runtime.child_started_at : null;
|
|
119
|
+
const childPidAlive = childPid !== null ? isPidAlive(childPid) : null;
|
|
120
|
+
// PID reuse heuristic: PID is alive but child_started_at is much older than
|
|
121
|
+
// the heartbeat threshold AND logs are stale. We cannot reliably read the
|
|
122
|
+
// OS process start time without platform-specific code, so we treat
|
|
123
|
+
// "PID alive + everything else stale" as a PID-reuse signal rather than
|
|
124
|
+
// "active". This is the conservative choice required by the safety contract.
|
|
125
|
+
const pidReuseSuspected = childPidAlive === true &&
|
|
126
|
+
heartbeatAgeSeconds !== null &&
|
|
127
|
+
heartbeatAgeSeconds > HEARTBEAT_STALE_SECONDS &&
|
|
128
|
+
(stdoutStat === null || stdoutStat.age > LOG_STALE_SECONDS) &&
|
|
129
|
+
(stderrStat === null || stderrStat.age > LOG_STALE_SECONDS);
|
|
130
|
+
const ownership = isWatcherOwningTask(taskDir, config);
|
|
131
|
+
const currentWatcherInstanceId = readWatcherInstanceId(config);
|
|
132
|
+
const collectingArtifactsPhase = phaseStr === "collecting_artifacts" || statusStr === "collecting_artifacts";
|
|
133
|
+
const taskStartedAt = typeof runtime.task_started_at === "string"
|
|
134
|
+
? runtime.task_started_at
|
|
135
|
+
: typeof status.started_at === "string"
|
|
136
|
+
? status.started_at
|
|
137
|
+
: null;
|
|
138
|
+
const statusUpdatedAt = typeof status.updated_at === "string" ? status.updated_at : null;
|
|
139
|
+
const evidence = {
|
|
140
|
+
heartbeat_age_seconds: heartbeatAgeSeconds,
|
|
141
|
+
stdout_age_seconds: stdoutStat?.age ?? null,
|
|
142
|
+
stderr_age_seconds: stderrStat?.age ?? null,
|
|
143
|
+
stdout_size_bytes: stdoutStat?.size ?? null,
|
|
144
|
+
stderr_size_bytes: stderrStat?.size ?? null,
|
|
145
|
+
child_pid: childPid,
|
|
146
|
+
child_pid_alive: childPidAlive,
|
|
147
|
+
child_started_at: childStartedAt,
|
|
148
|
+
pid_reuse_suspected: pidReuseSuspected,
|
|
149
|
+
watcher_instance_id: ownership.task_watcher_instance_id,
|
|
150
|
+
current_watcher_instance_id: currentWatcherInstanceId,
|
|
151
|
+
watcher_owns_task: ownership.owned,
|
|
152
|
+
watcher_ownership_reason: ownership.reason,
|
|
153
|
+
has_result_md: hasResultMd,
|
|
154
|
+
has_test_log: hasTestLog,
|
|
155
|
+
has_git_diff: hasGitDiff,
|
|
156
|
+
has_artifact_manifest: hasArtifactManifest,
|
|
157
|
+
task_started_at: taskStartedAt,
|
|
158
|
+
status_updated_at: statusUpdatedAt,
|
|
159
|
+
collecting_artifacts_phase: collectingArtifactsPhase,
|
|
160
|
+
};
|
|
161
|
+
// ── Decision tree ──
|
|
162
|
+
//
|
|
163
|
+
// The diagnosis must NEVER rely on a single signal. Each branch below
|
|
164
|
+
// combines at least two signals before producing a high-confidence
|
|
165
|
+
// conclusion. When signals conflict, confidence drops to medium/low.
|
|
166
|
+
const reasons = [];
|
|
167
|
+
const heartbeatStale = heartbeatAgeSeconds !== null && heartbeatAgeSeconds > HEARTBEAT_STALE_SECONDS;
|
|
168
|
+
const heartbeatPossiblyStale = heartbeatAgeSeconds !== null &&
|
|
169
|
+
heartbeatAgeSeconds > HEARTBEAT_POSSIBLY_STALE_SECONDS &&
|
|
170
|
+
heartbeatAgeSeconds <= HEARTBEAT_STALE_SECONDS;
|
|
171
|
+
const heartbeatFresh = heartbeatAgeSeconds !== null && heartbeatAgeSeconds <= HEARTBEAT_POSSIBLY_STALE_SECONDS;
|
|
172
|
+
const logsStale = (stdoutStat === null || stdoutStat.age > LOG_STALE_SECONDS) &&
|
|
173
|
+
(stderrStat === null || stderrStat.age > LOG_STALE_SECONDS);
|
|
174
|
+
const logsGrowing = stdoutStat !== null && stdoutStat.age <= HEARTBEAT_POSSIBLY_STALE_SECONDS;
|
|
175
|
+
// Collect artifact evidence strings
|
|
176
|
+
if (heartbeatStale)
|
|
177
|
+
reasons.push(`heartbeat older than ${HEARTBEAT_STALE_SECONDS}s (age: ${heartbeatAgeSeconds}s)`);
|
|
178
|
+
if (heartbeatPossiblyStale)
|
|
179
|
+
reasons.push(`heartbeat older than ${HEARTBEAT_POSSIBLY_STALE_SECONDS}s (age: ${heartbeatAgeSeconds}s)`);
|
|
180
|
+
if (stdoutStat && stdoutStat.age > LOG_STALE_SECONDS)
|
|
181
|
+
reasons.push(`stdout.log unchanged for ${stdoutStat.age}s`);
|
|
182
|
+
if (stderrStat && stderrStat.age > LOG_STALE_SECONDS)
|
|
183
|
+
reasons.push(`stderr.log unchanged for ${stderrStat.age}s`);
|
|
184
|
+
if (childPidAlive === false)
|
|
185
|
+
reasons.push(`child_pid ${childPid} is not alive`);
|
|
186
|
+
if (pidReuseSuspected)
|
|
187
|
+
reasons.push(`PID ${childPid} is alive but all other signals are stale — suspected PID reuse`);
|
|
188
|
+
if (!ownership.owned && ownership.reason === "instance_mismatch") {
|
|
189
|
+
reasons.push(`watcher_instance_id mismatch: task=${ownership.task_watcher_instance_id}, current=${ownership.current_watcher_instance_id}`);
|
|
190
|
+
}
|
|
191
|
+
if (!ownership.owned && ownership.reason === "watcher_missing") {
|
|
192
|
+
reasons.push("watcher heartbeat is missing — no live watcher owns this task");
|
|
193
|
+
}
|
|
194
|
+
if (!ownership.owned && ownership.reason === "no_runtime_record") {
|
|
195
|
+
reasons.push("task runtime has no watcher_instance_id recorded — ownership cannot be confirmed");
|
|
196
|
+
}
|
|
197
|
+
if (hasResultMd)
|
|
198
|
+
reasons.push("result.md already exists while status is still running");
|
|
199
|
+
if (hasTestLog)
|
|
200
|
+
reasons.push("test.log already exists while status is still running");
|
|
201
|
+
// ── done_candidate: artifacts exist but status not converged ──
|
|
202
|
+
// Strong signal: result.md + test.log + git.diff all exist, status still running.
|
|
203
|
+
// Medium signal: only result.md exists.
|
|
204
|
+
if (hasResultMd && (hasTestLog || hasGitDiff)) {
|
|
205
|
+
return buildResult(taskId, statusStr, phaseStr, "done_candidate", "high", reasons, [
|
|
206
|
+
"mark_done_by_agent",
|
|
207
|
+
"leave_unchanged",
|
|
208
|
+
], evidence, input.include_logs ? { stdoutPath, stderrPath } : null);
|
|
209
|
+
}
|
|
210
|
+
if (hasResultMd && collectingArtifactsPhase) {
|
|
211
|
+
return buildResult(taskId, statusStr, phaseStr, "done_candidate", "medium", reasons, [
|
|
212
|
+
"mark_done_by_agent",
|
|
213
|
+
"collect_artifacts",
|
|
214
|
+
"leave_unchanged",
|
|
215
|
+
], evidence, input.include_logs ? { stdoutPath, stderrPath } : null);
|
|
216
|
+
}
|
|
217
|
+
// ── orphaned_running: watcher does not own the task ──
|
|
218
|
+
if (!ownership.owned && (ownership.reason === "instance_mismatch" || ownership.reason === "watcher_missing")) {
|
|
219
|
+
return buildResult(taskId, statusStr, phaseStr, "orphaned_running", "high", reasons, [
|
|
220
|
+
"mark_orphaned",
|
|
221
|
+
"leave_unchanged",
|
|
222
|
+
], evidence, input.include_logs ? { stdoutPath, stderrPath } : null);
|
|
223
|
+
}
|
|
224
|
+
// ── artifact_collection_stuck: in collecting_artifacts phase and stuck ──
|
|
225
|
+
if (collectingArtifactsPhase) {
|
|
226
|
+
const collectionStuck = (heartbeatStale || heartbeatPossiblyStale) && (logsStale || childPidAlive === false);
|
|
227
|
+
if (collectionStuck) {
|
|
228
|
+
return buildResult(taskId, statusStr, phaseStr, "artifact_collection_stuck", "high", reasons, [
|
|
229
|
+
"recollect_artifacts",
|
|
230
|
+
"mark_failed_stale",
|
|
231
|
+
"leave_unchanged",
|
|
232
|
+
], evidence, input.include_logs ? { stdoutPath, stderrPath } : null);
|
|
233
|
+
}
|
|
234
|
+
// Collecting but not clearly stuck — possibly still active
|
|
235
|
+
if (heartbeatFresh || logsGrowing) {
|
|
236
|
+
return buildResult(taskId, statusStr, phaseStr, "active_running", "medium", reasons, [
|
|
237
|
+
"leave_unchanged",
|
|
238
|
+
], evidence, input.include_logs ? { stdoutPath, stderrPath } : null);
|
|
239
|
+
}
|
|
240
|
+
return buildResult(taskId, statusStr, phaseStr, "artifact_collection_stuck", "medium", reasons, [
|
|
241
|
+
"recollect_artifacts",
|
|
242
|
+
"leave_unchanged",
|
|
243
|
+
], evidence, input.include_logs ? { stdoutPath, stderrPath } : null);
|
|
244
|
+
}
|
|
245
|
+
// ── active_running: heartbeat fresh, watcher owns, logs growing or PID alive ──
|
|
246
|
+
if (heartbeatFresh && ownership.owned && (logsGrowing || childPidAlive === true)) {
|
|
247
|
+
return buildResult(taskId, statusStr, phaseStr, "active_running", "high", reasons, [
|
|
248
|
+
"leave_unchanged",
|
|
249
|
+
], evidence, input.include_logs ? { stdoutPath, stderrPath } : null);
|
|
250
|
+
}
|
|
251
|
+
// ── stale_running: heartbeat stale, PID dead, watcher doesn't own ──
|
|
252
|
+
if (heartbeatStale && (childPidAlive === false || !ownership.owned) && logsStale) {
|
|
253
|
+
return buildResult(taskId, statusStr, phaseStr, "stale_running", "high", reasons, [
|
|
254
|
+
"mark_failed_stale",
|
|
255
|
+
"collect_artifacts",
|
|
256
|
+
"leave_unchanged",
|
|
257
|
+
], evidence, input.include_logs ? { stdoutPath, stderrPath } : null);
|
|
258
|
+
}
|
|
259
|
+
// ── possibly_stale_running: some stale signals but not enough for high confidence ──
|
|
260
|
+
// This branch catches the PID-reuse-suspected case as well: PID is alive
|
|
261
|
+
// but everything else is stale. We refuse to call it "active".
|
|
262
|
+
if (pidReuseSuspected) {
|
|
263
|
+
return buildResult(taskId, statusStr, phaseStr, "possibly_stale_running", "medium", reasons, [
|
|
264
|
+
"leave_unchanged",
|
|
265
|
+
"mark_failed_stale",
|
|
266
|
+
], evidence, input.include_logs ? { stdoutPath, stderrPath } : null);
|
|
267
|
+
}
|
|
268
|
+
if (heartbeatStale || (heartbeatPossiblyStale && (logsStale || !ownership.owned))) {
|
|
269
|
+
return buildResult(taskId, statusStr, phaseStr, "possibly_stale_running", "medium", reasons, [
|
|
270
|
+
"leave_unchanged",
|
|
271
|
+
"mark_failed_stale",
|
|
272
|
+
], evidence, input.include_logs ? { stdoutPath, stderrPath } : null);
|
|
273
|
+
}
|
|
274
|
+
// ── unknown: insufficient evidence ──
|
|
275
|
+
return buildResult(taskId, statusStr, phaseStr, "unknown", "low", reasons, [
|
|
276
|
+
"leave_unchanged",
|
|
277
|
+
], evidence, input.include_logs ? { stdoutPath, stderrPath } : null);
|
|
278
|
+
}
|
|
279
|
+
// ── Result builders ─────────────────────────────────────────────────
|
|
280
|
+
function buildResult(taskId, status, phase, diagnosis, confidence, reasons, safeActions, evidence, logPaths) {
|
|
281
|
+
return {
|
|
282
|
+
task_id: taskId,
|
|
283
|
+
status,
|
|
284
|
+
phase,
|
|
285
|
+
diagnosis,
|
|
286
|
+
confidence,
|
|
287
|
+
reasons,
|
|
288
|
+
safe_actions: safeActions,
|
|
289
|
+
evidence,
|
|
290
|
+
logs: logPaths
|
|
291
|
+
? {
|
|
292
|
+
stdout_tail: readTailLines(logPaths.stdoutPath, LOG_TAIL_LINES),
|
|
293
|
+
stderr_tail: readTailLines(logPaths.stderrPath, Math.min(LOG_TAIL_LINES, 20)),
|
|
294
|
+
}
|
|
295
|
+
: null,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function buildTerminalDiagnosis(taskId, status, phase) {
|
|
299
|
+
return {
|
|
300
|
+
task_id: taskId,
|
|
301
|
+
status,
|
|
302
|
+
phase,
|
|
303
|
+
diagnosis: "terminal",
|
|
304
|
+
confidence: "high",
|
|
305
|
+
reasons: [`status "${status}" is already terminal; no reconciliation needed`],
|
|
306
|
+
safe_actions: ["leave_unchanged"],
|
|
307
|
+
evidence: emptyEvidence(),
|
|
308
|
+
logs: null,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function buildUnknownDiagnosis(taskId, status, phase, extraReasons) {
|
|
312
|
+
return {
|
|
313
|
+
task_id: taskId,
|
|
314
|
+
status,
|
|
315
|
+
phase,
|
|
316
|
+
diagnosis: "unknown",
|
|
317
|
+
confidence: "low",
|
|
318
|
+
reasons: ["insufficient evidence to diagnose", ...extraReasons],
|
|
319
|
+
safe_actions: ["leave_unchanged"],
|
|
320
|
+
evidence: emptyEvidence(),
|
|
321
|
+
logs: null,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function emptyEvidence() {
|
|
325
|
+
return {
|
|
326
|
+
heartbeat_age_seconds: null,
|
|
327
|
+
stdout_age_seconds: null,
|
|
328
|
+
stderr_age_seconds: null,
|
|
329
|
+
stdout_size_bytes: null,
|
|
330
|
+
stderr_size_bytes: null,
|
|
331
|
+
child_pid: null,
|
|
332
|
+
child_pid_alive: null,
|
|
333
|
+
child_started_at: null,
|
|
334
|
+
pid_reuse_suspected: false,
|
|
335
|
+
watcher_instance_id: null,
|
|
336
|
+
current_watcher_instance_id: null,
|
|
337
|
+
watcher_owns_task: false,
|
|
338
|
+
watcher_ownership_reason: "no_runtime_record",
|
|
339
|
+
has_result_md: false,
|
|
340
|
+
has_test_log: false,
|
|
341
|
+
has_git_diff: false,
|
|
342
|
+
has_artifact_manifest: false,
|
|
343
|
+
task_started_at: null,
|
|
344
|
+
status_updated_at: null,
|
|
345
|
+
collecting_artifacts_phase: false,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v1.0.0 Part B: discard_worktree MCP 工具 handler — 移除隔离 worktree 与临时
|
|
3
|
+
* branch,并将最终状态归档到 .patchwarden/worktree-archive/。
|
|
4
|
+
*
|
|
5
|
+
* 参考 src/tools/auditTask.ts 的导出/返回风格。
|
|
6
|
+
*/
|
|
7
|
+
export interface DiscardWorktreeInput {
|
|
8
|
+
worktree_id: string;
|
|
9
|
+
repo_path: string;
|
|
10
|
+
}
|
|
11
|
+
export interface DiscardWorktreeOutput {
|
|
12
|
+
worktree_id: string;
|
|
13
|
+
status: "discarded";
|
|
14
|
+
discarded_at: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 丢弃指定 worktree:移除 worktree 目录、删除临时 branch、归档状态。
|
|
18
|
+
*
|
|
19
|
+
* @throws PatchWardenError("workspace_path_escape") 当 repo_path 逃逸出 workspaceRoot
|
|
20
|
+
* @throws PatchWardenError("worktree_not_found") 当 worktree 不存在
|
|
21
|
+
* @throws PatchWardenError("invalid_worktree_state") 当 worktree 非 active
|
|
22
|
+
* @throws PatchWardenError("worktree_discard_failed") 当 git worktree remove 失败
|
|
23
|
+
*/
|
|
24
|
+
export declare function discardWorktreeTool(input: DiscardWorktreeInput): DiscardWorktreeOutput;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v1.0.0 Part B: discard_worktree MCP 工具 handler — 移除隔离 worktree 与临时
|
|
3
|
+
* branch,并将最终状态归档到 .patchwarden/worktree-archive/。
|
|
4
|
+
*
|
|
5
|
+
* 参考 src/tools/auditTask.ts 的导出/返回风格。
|
|
6
|
+
*/
|
|
7
|
+
import { getConfig } from "../config.js";
|
|
8
|
+
import { guardWorkspacePath } from "../security/pathGuard.js";
|
|
9
|
+
import { discardWorktree } from "../goal/worktreeManager.js";
|
|
10
|
+
/**
|
|
11
|
+
* 丢弃指定 worktree:移除 worktree 目录、删除临时 branch、归档状态。
|
|
12
|
+
*
|
|
13
|
+
* @throws PatchWardenError("workspace_path_escape") 当 repo_path 逃逸出 workspaceRoot
|
|
14
|
+
* @throws PatchWardenError("worktree_not_found") 当 worktree 不存在
|
|
15
|
+
* @throws PatchWardenError("invalid_worktree_state") 当 worktree 非 active
|
|
16
|
+
* @throws PatchWardenError("worktree_discard_failed") 当 git worktree remove 失败
|
|
17
|
+
*/
|
|
18
|
+
export function discardWorktreeTool(input) {
|
|
19
|
+
const config = getConfig();
|
|
20
|
+
const repoPathSafe = guardWorkspacePath(input.repo_path, config.workspaceRoot);
|
|
21
|
+
discardWorktree(input.worktree_id, repoPathSafe);
|
|
22
|
+
return {
|
|
23
|
+
worktree_id: input.worktree_id,
|
|
24
|
+
status: "discarded",
|
|
25
|
+
discarded_at: new Date().toISOString(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.7.1: discover_tools MCP 工具封装。
|
|
3
|
+
*
|
|
4
|
+
* 搜索候选工具,返回压缩摘要和风险等级。
|
|
5
|
+
* 不加入 chatgpt_core,仅 full profile。
|
|
6
|
+
* v0.8.1: 为每个搜索结果生成 discoveryToken(server-side 保存)。
|
|
7
|
+
* v0.9.0: 传入 usageStatsProvider,从 invocation.log 聚合历史成功率用于混合排序。
|
|
8
|
+
*/
|
|
9
|
+
import type { ToolDef } from "./registry.js";
|
|
10
|
+
import { discoverTools as searchDiscover, type DiscoverToolsInput } from "./toolSearch.js";
|
|
11
|
+
export declare function discoverTools(input: DiscoverToolsInput, tools: ToolDef[]): ReturnType<typeof searchDiscover>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.7.1: discover_tools MCP 工具封装。
|
|
3
|
+
*
|
|
4
|
+
* 搜索候选工具,返回压缩摘要和风险等级。
|
|
5
|
+
* 不加入 chatgpt_core,仅 full profile。
|
|
6
|
+
* v0.8.1: 为每个搜索结果生成 discoveryToken(server-side 保存)。
|
|
7
|
+
* v0.9.0: 传入 usageStatsProvider,从 invocation.log 聚合历史成功率用于混合排序。
|
|
8
|
+
*/
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { buildToolRegistry } from "./toolRegistry.js";
|
|
11
|
+
import { discoverTools as searchDiscover } from "./toolSearch.js";
|
|
12
|
+
import { issueToken } from "../security/discoveryTokenStore.js";
|
|
13
|
+
import { aggregateUsageStats } from "./toolUsageStats.js";
|
|
14
|
+
import { getConfig } from "../config.js";
|
|
15
|
+
export function discoverTools(input, tools) {
|
|
16
|
+
const registry = buildToolRegistry(tools);
|
|
17
|
+
// v0.9.0: 从 invocation.log 聚合历史成功率,用于混合排序公式
|
|
18
|
+
let usageMap = null;
|
|
19
|
+
try {
|
|
20
|
+
const workspaceRoot = getConfig().workspaceRoot;
|
|
21
|
+
const logsDir = join(workspaceRoot, ".patchwarden", "logs");
|
|
22
|
+
usageMap = aggregateUsageStats(logsDir);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// 配置读取失败或日志目录不存在时,无历史数据(不阻断搜索)
|
|
26
|
+
usageMap = null;
|
|
27
|
+
}
|
|
28
|
+
return searchDiscover(input, registry, (tool) => {
|
|
29
|
+
return issueToken({
|
|
30
|
+
toolName: tool.name,
|
|
31
|
+
risk: tool.risk,
|
|
32
|
+
query: input.query,
|
|
33
|
+
schemaDigest: tool.inputSchemaDigest,
|
|
34
|
+
profile: input.profile ?? "full",
|
|
35
|
+
});
|
|
36
|
+
}, usageMap
|
|
37
|
+
? (toolName) => usageMap.get(toolName) ?? null
|
|
38
|
+
: undefined);
|
|
39
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.7.1: explain_tool MCP 工具封装。
|
|
3
|
+
*
|
|
4
|
+
* 展开单个工具详情,可选包含完整 inputSchema。
|
|
5
|
+
* 不加入 chatgpt_core,仅 full profile。
|
|
6
|
+
* v0.8.1: includeSchema=true 时通过 tokenPeeker 二次确认 schema digest,
|
|
7
|
+
* 检测 registry 与实际 toolDef 之间的 schema drift。
|
|
8
|
+
*/
|
|
9
|
+
import type { ToolDef } from "./registry.js";
|
|
10
|
+
import { explainTool as searchExplain, type ExplainToolInput } from "./toolSearch.js";
|
|
11
|
+
export declare function explainTool(input: ExplainToolInput, tools: ToolDef[]): ReturnType<typeof searchExplain>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.7.1: explain_tool MCP 工具封装。
|
|
3
|
+
*
|
|
4
|
+
* 展开单个工具详情,可选包含完整 inputSchema。
|
|
5
|
+
* 不加入 chatgpt_core,仅 full profile。
|
|
6
|
+
* v0.8.1: includeSchema=true 时通过 tokenPeeker 二次确认 schema digest,
|
|
7
|
+
* 检测 registry 与实际 toolDef 之间的 schema drift。
|
|
8
|
+
*/
|
|
9
|
+
import { buildToolRegistry, computeSchemaDigest } from "./toolRegistry.js";
|
|
10
|
+
import { explainTool as searchExplain } from "./toolSearch.js";
|
|
11
|
+
export function explainTool(input, tools) {
|
|
12
|
+
const registry = buildToolRegistry(tools);
|
|
13
|
+
const toolDefsMap = new Map(tools.map((t) => [t.name, t]));
|
|
14
|
+
return searchExplain(input, registry, toolDefsMap, (toolName) => {
|
|
15
|
+
const toolDef = toolDefsMap.get(toolName);
|
|
16
|
+
if (!toolDef)
|
|
17
|
+
return null;
|
|
18
|
+
const currentDigest = computeSchemaDigest(toolDef.inputSchema);
|
|
19
|
+
return { schemaDigest: currentDigest };
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -6,6 +6,7 @@ import { getTaskStatus } from "./getTaskStatus.js";
|
|
|
6
6
|
import { redactSensitiveValue } from "../security/contentRedaction.js";
|
|
7
7
|
const TERMINAL_STATUSES = new Set([
|
|
8
8
|
"done",
|
|
9
|
+
"done_by_agent",
|
|
9
10
|
"failed",
|
|
10
11
|
"failed_verification",
|
|
11
12
|
"failed_scope_violation",
|
|
@@ -80,7 +81,7 @@ export function getTaskSummary(taskId, options = {}) {
|
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
else if (terminal) {
|
|
83
|
-
if (status.status !== "done" || outOfScope.length > 0 || verifyStatus === "failed") {
|
|
84
|
+
if ((status.status !== "done" && status.status !== "done_by_agent") || outOfScope.length > 0 || verifyStatus === "failed") {
|
|
84
85
|
acceptanceStatus = "failed";
|
|
85
86
|
}
|
|
86
87
|
else if (verifyStatus === "passed") {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.8.0: createSubgoalTask — 原子地将一个新 subgoal 关联到一个新任务。
|
|
3
|
+
*
|
|
4
|
+
* 流程(原子语义):
|
|
5
|
+
* 1. 读取 goal_status.json(不存在则抛 goal_not_found)
|
|
6
|
+
* 2. addSubgoal(校验 depends_on 引用已有 subgoal)
|
|
7
|
+
* 3. createTask 创建关联任务(写入 goal_id / subgoal_id 到 task status.json)
|
|
8
|
+
* 4. linkTaskToSubgoal(将 task_id 关联到 subgoal)
|
|
9
|
+
* 5. updateSubgoalStatus(running)(ready → running)
|
|
10
|
+
* 6. 写回 goal_status.json
|
|
11
|
+
*
|
|
12
|
+
* 注意:
|
|
13
|
+
* - createSubgoalTask 强制 execution_mode 为 "execute",因为 assess_only 不产生可关联的 task。
|
|
14
|
+
* - createTask 内部使用 getConfig(),因此 workspaceRoot 由全局配置决定。
|
|
15
|
+
*/
|
|
16
|
+
import { type CreateTaskInput } from "./createTask.js";
|
|
17
|
+
export interface CreateSubgoalTaskInput {
|
|
18
|
+
goal_id: string;
|
|
19
|
+
subgoal_title: string;
|
|
20
|
+
depends_on?: string[];
|
|
21
|
+
plan_id?: string;
|
|
22
|
+
inline_plan?: string;
|
|
23
|
+
plan_title?: string;
|
|
24
|
+
template?: CreateTaskInput["template"];
|
|
25
|
+
goal?: string;
|
|
26
|
+
agent?: string;
|
|
27
|
+
repo_path: string;
|
|
28
|
+
test_command?: string;
|
|
29
|
+
verify_commands?: string[];
|
|
30
|
+
timeout_seconds?: number;
|
|
31
|
+
execution_mode?: "assess_only" | "execute";
|
|
32
|
+
assessment_id?: string;
|
|
33
|
+
scope?: string[];
|
|
34
|
+
forbidden?: string[];
|
|
35
|
+
verification?: string[];
|
|
36
|
+
done_evidence?: string[];
|
|
37
|
+
isolate_worktree?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface CreateSubgoalTaskOutput {
|
|
40
|
+
subgoal_id: string;
|
|
41
|
+
task_id: string;
|
|
42
|
+
subgoal_status: "running";
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 原子地创建一个 subgoal 并关联到一个新任务。
|
|
46
|
+
*
|
|
47
|
+
* @throws PatchWardenError("goal_not_found") 当 goal_id 不存在
|
|
48
|
+
* @throws PatchWardenError("invalid_dependency") 当 depends_on 引用不存在的 subgoal
|
|
49
|
+
* @throws PatchWardenError("invalid_execution_mode") 当 execution_mode 为 "assess_only"
|
|
50
|
+
*/
|
|
51
|
+
export declare function createSubgoalTask(input: CreateSubgoalTaskInput): CreateSubgoalTaskOutput;
|