patchwarden 0.6.4 → 1.5.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/README.en.md +54 -20
- package/README.md +48 -21
- 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.js +212 -3
- package/dist/direct/directSessionStore.d.ts +2 -0
- package/dist/direct/directVerification.js +7 -0
- package/dist/doctor.js +32 -1
- 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/policy/projectPolicy.d.ts +55 -0
- package/dist/policy/projectPolicy.js +286 -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/evidence-pack.test.d.ts +1 -0
- package/dist/test/unit/evidence-pack.test.js +130 -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/project-policy-release-mode.test.d.ts +1 -0
- package/dist/test/unit/project-policy-release-mode.test.js +125 -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/run-task-loop.test.d.ts +1 -0
- package/dist/test/unit/run-task-loop.test.js +380 -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 +176 -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/evidencePack.d.ts +39 -0
- package/dist/tools/evidencePack.js +168 -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/recommendAgentForTask.d.ts +19 -0
- package/dist/tools/recommendAgentForTask.js +56 -0
- package/dist/tools/reconcileTasks.d.ts +41 -0
- package/dist/tools/reconcileTasks.js +352 -0
- package/dist/tools/registry.js +926 -2
- package/dist/tools/releaseMode.d.ts +50 -0
- package/dist/tools/releaseMode.js +370 -0
- package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
- package/dist/tools/runDirectVerificationBundle.js +64 -0
- package/dist/tools/runTaskLoop.d.ts +57 -0
- package/dist/tools/runTaskLoop.js +417 -0
- package/dist/tools/runVerification.d.ts +4 -0
- package/dist/tools/runVerification.js +4 -0
- package/dist/tools/safeStatus.d.ts +31 -1
- package/dist/tools/safeStatus.js +43 -1
- package/dist/tools/safeViews.d.ts +262 -0
- package/dist/tools/safeViews.js +252 -0
- package/dist/tools/schemaDriftCheck.d.ts +46 -0
- package/dist/tools/schemaDriftCheck.js +80 -0
- package/dist/tools/taskLineage.d.ts +91 -0
- package/dist/tools/taskLineage.js +175 -0
- package/dist/tools/toolCatalog.d.ts +4 -3
- package/dist/tools/toolCatalog.js +39 -11
- package/dist/tools/toolRegistry.d.ts +61 -0
- package/dist/tools/toolRegistry.js +834 -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/chatgpt-usage.md +31 -0
- package/docs/control-center/README.md +9 -0
- package/package.json +3 -2
- package/scripts/checks/control-center-smoke.js +87 -0
- package/scripts/checks/control-smoke.js +2 -2
- package/scripts/checks/doctor-smoke.js +1 -0
- package/scripts/checks/lifecycle-smoke.js +13 -9
- package/scripts/checks/mcp-manifest-check.js +65 -6
- package/scripts/checks/mcp-smoke.js +60 -9
- package/scripts/checks/watcher-supervisor-smoke.js +2 -2
- package/src/agents/agentRouter.ts +149 -0
- package/src/config.ts +9 -3
- package/src/controlCenter.ts +215 -4
- package/src/direct/directSessionStore.ts +2 -0
- package/src/direct/directVerification.ts +7 -0
- package/src/doctor.ts +36 -1
- 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/policy/projectPolicy.ts +344 -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/evidence-pack.test.ts +142 -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/project-policy-release-mode.test.ts +156 -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/run-task-loop.test.ts +425 -0
- package/src/test/unit/safe-views.test.ts +184 -0
- package/src/test/unit/schema-drift-check.test.ts +260 -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/evidencePack.ts +205 -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/listWorkspace.ts +71 -71
- package/src/tools/mergeWorktree.ts +42 -0
- package/src/tools/recommendAgentForTask.ts +79 -0
- package/src/tools/reconcileTasks.ts +464 -0
- package/src/tools/registry.ts +1093 -73
- package/src/tools/releaseMode.ts +450 -0
- package/src/tools/runDirectVerificationBundle.ts +98 -0
- package/src/tools/runTaskLoop.ts +526 -0
- package/src/tools/runVerification.ts +8 -0
- package/src/tools/safeStatus.ts +73 -2
- package/src/tools/safeViews.ts +273 -0
- package/src/tools/schemaDriftCheck.ts +120 -0
- package/src/tools/taskLineage.ts +300 -0
- package/src/tools/toolCatalog.ts +40 -11
- package/src/tools/toolRegistry.ts +896 -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/pages/dashboard.html +143 -2
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
|
|
5
|
+
const EXCLUDED_DIRS = new Set([".git", ".patchwarden", ".venv", "node_modules", "samples", "docs"]);
|
|
6
|
+
const CLEANUP_DIRS = new Set(["__pycache__", "dist", "release_packages", "temp_files"]);
|
|
7
|
+
const CLEANUP_FILES = [/\.pyc$/i];
|
|
8
|
+
|
|
9
|
+
export interface PostTaskCleanupEntry {
|
|
10
|
+
path: string;
|
|
11
|
+
reason: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PostTaskCleanupReport {
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
removed: PostTaskCleanupEntry[];
|
|
17
|
+
skipped: Array<PostTaskCleanupEntry & { skip_reason: string }>;
|
|
18
|
+
source_files_touched: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function runPostTaskCleanup(repoPath: string, taskDir: string): PostTaskCleanupReport {
|
|
22
|
+
const report: PostTaskCleanupReport = {
|
|
23
|
+
enabled: true,
|
|
24
|
+
removed: [],
|
|
25
|
+
skipped: [],
|
|
26
|
+
source_files_touched: 0,
|
|
27
|
+
};
|
|
28
|
+
const root = resolve(repoPath);
|
|
29
|
+
const candidates = collectCandidates(root);
|
|
30
|
+
for (const candidate of candidates) {
|
|
31
|
+
const rel = toRepoRelative(root, candidate.path);
|
|
32
|
+
if (!rel || isExcluded(rel)) {
|
|
33
|
+
report.skipped.push({ path: rel || ".", reason: candidate.reason, skip_reason: "excluded_path" });
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (hasTrackedGitContent(root, rel)) {
|
|
37
|
+
report.skipped.push({ path: rel, reason: candidate.reason, skip_reason: "tracked_by_git" });
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (!isIgnoredOrUntracked(root, rel)) {
|
|
41
|
+
report.skipped.push({ path: rel, reason: candidate.reason, skip_reason: "not_ignored_or_untracked" });
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
rmSync(candidate.path, { recursive: true, force: true });
|
|
46
|
+
report.removed.push({ path: rel, reason: candidate.reason });
|
|
47
|
+
} catch (error) {
|
|
48
|
+
report.skipped.push({
|
|
49
|
+
path: rel,
|
|
50
|
+
reason: candidate.reason,
|
|
51
|
+
skip_reason: error instanceof Error ? error.message : String(error),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
writeFileSync(join(taskDir, "post-task-cleanup.json"), JSON.stringify(report, null, 2), "utf-8");
|
|
56
|
+
return report;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function collectCandidates(root: string): PostTaskCleanupEntry[] {
|
|
60
|
+
const found = new Map<string, string>();
|
|
61
|
+
walk(root, (path, isDir) => {
|
|
62
|
+
const name = basename(path);
|
|
63
|
+
if (isDir) {
|
|
64
|
+
if (name === "__pycache__") found.set(path, "python_bytecode_cache");
|
|
65
|
+
if (name === "dist" && toRepoRelative(root, path) === "frontend/dist") found.set(path, "frontend_build_output");
|
|
66
|
+
if (name === "release_packages") found.set(path, "release_package_output");
|
|
67
|
+
if (name === "temp_files" && toRepoRelative(root, path) === "backend/temp_files") found.set(path, "backend_temp_files");
|
|
68
|
+
} else if (CLEANUP_FILES.some((pattern) => pattern.test(name))) {
|
|
69
|
+
found.set(path, "python_bytecode_file");
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return [...found].map(([path, reason]) => ({ path, reason }));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function walk(dir: string, visit: (path: string, isDir: boolean) => void): void {
|
|
76
|
+
if (!existsSync(dir)) return;
|
|
77
|
+
let entries;
|
|
78
|
+
try {
|
|
79
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
80
|
+
} catch {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
const full = join(dir, entry.name);
|
|
85
|
+
const isDir = entry.isDirectory();
|
|
86
|
+
visit(full, isDir);
|
|
87
|
+
if (isDir && !EXCLUDED_DIRS.has(entry.name) && !CLEANUP_DIRS.has(entry.name)) {
|
|
88
|
+
walk(full, visit);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hasTrackedGitContent(root: string, rel: string): boolean {
|
|
94
|
+
if (!isGitWorktree(root)) return false;
|
|
95
|
+
try {
|
|
96
|
+
const output = execFileSync("git", ["ls-files", "--", rel], {
|
|
97
|
+
cwd: root,
|
|
98
|
+
encoding: "utf-8",
|
|
99
|
+
windowsHide: true,
|
|
100
|
+
});
|
|
101
|
+
return output.trim().length > 0;
|
|
102
|
+
} catch {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isIgnoredOrUntracked(root: string, rel: string): boolean {
|
|
108
|
+
if (!isGitWorktree(root)) return true;
|
|
109
|
+
try {
|
|
110
|
+
execFileSync("git", ["check-ignore", "-q", "--", rel], {
|
|
111
|
+
cwd: root,
|
|
112
|
+
stdio: "ignore",
|
|
113
|
+
windowsHide: true,
|
|
114
|
+
});
|
|
115
|
+
return true;
|
|
116
|
+
} catch {
|
|
117
|
+
try {
|
|
118
|
+
const output = execFileSync("git", ["ls-files", "--others", "--exclude-standard", "--", rel], {
|
|
119
|
+
cwd: root,
|
|
120
|
+
encoding: "utf-8",
|
|
121
|
+
windowsHide: true,
|
|
122
|
+
});
|
|
123
|
+
return output.trim().length > 0 || !existsSync(resolve(root, rel));
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isGitWorktree(root: string): boolean {
|
|
131
|
+
try {
|
|
132
|
+
return execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
133
|
+
cwd: root,
|
|
134
|
+
encoding: "utf-8",
|
|
135
|
+
windowsHide: true,
|
|
136
|
+
}).trim() === "true";
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function toRepoRelative(root: string, target: string): string {
|
|
143
|
+
const resolved = resolve(target);
|
|
144
|
+
const rel = relative(root, resolved);
|
|
145
|
+
if (isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`)) {
|
|
146
|
+
return "";
|
|
147
|
+
}
|
|
148
|
+
return rel.replace(/\\/g, "/");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isExcluded(rel: string): boolean {
|
|
152
|
+
const parts = rel.split("/");
|
|
153
|
+
return parts.some((part) => EXCLUDED_DIRS.has(part));
|
|
154
|
+
}
|
package/src/runner/runTask.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from "./changeCapture.js";
|
|
35
35
|
import { PatchWardenError, errorPayload } from "../errors.js";
|
|
36
36
|
import { diagnoseAndroidBuild } from "../tools/androidDoctor.js";
|
|
37
|
+
import { runPostTaskCleanup, type PostTaskCleanupReport } from "./postTaskCleanup.js";
|
|
37
38
|
|
|
38
39
|
const HEARTBEAT_INTERVAL_MS = 2000;
|
|
39
40
|
const GRACEFUL_KILL_MS = 2000;
|
|
@@ -150,6 +151,14 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
150
151
|
current_command: null,
|
|
151
152
|
error: null,
|
|
152
153
|
});
|
|
154
|
+
// v0.7.0: record task_started_at and watcher_instance_id so diagnose_task
|
|
155
|
+
// can detect PID reuse and orphaned tasks. watcher_instance_id comes from
|
|
156
|
+
// the watcher process env; when run_task MCP tool is used directly, this
|
|
157
|
+
// field is intentionally left undefined so ownership cannot be falsely claimed.
|
|
158
|
+
writeTaskRuntime(taskDir, {
|
|
159
|
+
task_started_at: new Date(startedAtMs).toISOString(),
|
|
160
|
+
watcher_instance_id: process.env.PATCHWARDEN_WATCHER_INSTANCE_ID || undefined,
|
|
161
|
+
});
|
|
153
162
|
setTaskPhase(taskDir, "preparing", null, "Capturing pre-task repository state.");
|
|
154
163
|
|
|
155
164
|
let beforeSnapshot;
|
|
@@ -230,19 +239,40 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
230
239
|
? `Verification command \"${failedVerification.command}\" could not start: ${failedVerification.spawnError}`
|
|
231
240
|
: `Verification command \"${failedVerification.command}\" exited with code ${failedVerification.exitCode}.`;
|
|
232
241
|
} else if (verifyResults.length === verifyCommands.length) {
|
|
233
|
-
finalStatus = "
|
|
242
|
+
finalStatus = "done_by_agent";
|
|
234
243
|
} else {
|
|
235
244
|
finalError = "Verification did not complete all configured commands.";
|
|
236
245
|
}
|
|
237
246
|
} else {
|
|
238
247
|
testResult = skippedTest(testCommand, repoPath, "No verification command configured.");
|
|
239
|
-
finalStatus = "
|
|
248
|
+
finalStatus = "done_by_agent";
|
|
240
249
|
}
|
|
241
250
|
} catch (error) {
|
|
242
251
|
lastCaughtError = error;
|
|
243
252
|
finalError = errorMessage(error);
|
|
244
253
|
}
|
|
245
254
|
|
|
255
|
+
let cleanupReport: PostTaskCleanupReport = {
|
|
256
|
+
enabled: true,
|
|
257
|
+
removed: [],
|
|
258
|
+
skipped: [],
|
|
259
|
+
source_files_touched: 0,
|
|
260
|
+
};
|
|
261
|
+
if (finalStatus !== "canceled") {
|
|
262
|
+
try {
|
|
263
|
+
cleanupReport = runPostTaskCleanup(repoPath, taskDir);
|
|
264
|
+
updateStatus(taskDir, { cleanup: cleanupReport });
|
|
265
|
+
} catch (error) {
|
|
266
|
+
cleanupReport = {
|
|
267
|
+
enabled: true,
|
|
268
|
+
removed: [],
|
|
269
|
+
skipped: [{ path: ".", reason: "post_task_cleanup", skip_reason: errorMessage(error) }],
|
|
270
|
+
source_files_touched: 0,
|
|
271
|
+
};
|
|
272
|
+
writeFileSync(join(taskDir, "post-task-cleanup.json"), JSON.stringify(cleanupReport, null, 2), "utf-8");
|
|
273
|
+
updateStatus(taskDir, { cleanup: cleanupReport });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
246
276
|
setTaskPhase(taskDir, "collecting_artifacts", null, "Capturing post-task state and writing reports.");
|
|
247
277
|
const artifactCollectionStartedAt = new Date().toISOString();
|
|
248
278
|
let changes: ChangeArtifacts;
|
|
@@ -310,7 +340,7 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
310
340
|
"",
|
|
311
341
|
].join("\n"), "utf-8");
|
|
312
342
|
// Don't override test failure with artifact failure
|
|
313
|
-
if (finalStatus === "
|
|
343
|
+
if (finalStatus === "done_by_agent") {
|
|
314
344
|
finalError = `Change capture failed: ${artifactCollectionError}`;
|
|
315
345
|
finalStatus = "failed";
|
|
316
346
|
} else {
|
|
@@ -335,7 +365,7 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
335
365
|
);
|
|
336
366
|
} catch (error) {
|
|
337
367
|
finalError ||= `Workspace scope capture failed: ${errorMessage(error)}`;
|
|
338
|
-
if (finalStatus === "
|
|
368
|
+
if (finalStatus === "done_by_agent") finalStatus = "failed";
|
|
339
369
|
}
|
|
340
370
|
|
|
341
371
|
// Phase 4: Pre-existing external dirty files are warnings, not failures
|
|
@@ -400,8 +430,8 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
400
430
|
writeFileSync(join(taskDir, "verify.json"), JSON.stringify(verifyJson, null, 2), "utf-8");
|
|
401
431
|
writeFileSync(join(taskDir, "verify.log"), buildVerifyLog(verifyJson.commands), "utf-8");
|
|
402
432
|
|
|
403
|
-
if (!["canceled", "
|
|
404
|
-
const finalPhase: TaskPhase = finalStatus === "
|
|
433
|
+
if (!["canceled", "done_by_agent", "failed_verification", "failed_scope_violation", "failed_policy_violation"].includes(finalStatus)) finalStatus = "failed";
|
|
434
|
+
const finalPhase: TaskPhase = finalStatus === "done_by_agent" ? "done_by_agent" : finalStatus;
|
|
405
435
|
const followup = buildFailureFollowup(finalStatus, finalError, verifyJson.commands);
|
|
406
436
|
|
|
407
437
|
// Phase 7: Run Android build environment diagnostics if android_app exists
|
|
@@ -462,6 +492,7 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
462
492
|
artifact_collection_error: artifactCollectionError,
|
|
463
493
|
artifact_collection_started_at: artifactCollectionStartedAt,
|
|
464
494
|
artifact_collection_finished_at: artifactCollectionFinishedAt,
|
|
495
|
+
cleanup: cleanupReport,
|
|
465
496
|
artifact_manifest: artifactManifest,
|
|
466
497
|
out_of_scope_changes: outOfScopeChanges,
|
|
467
498
|
new_out_of_scope_changes: newOutOfScopeChanges,
|
|
@@ -479,7 +510,7 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
479
510
|
commands_observed: [],
|
|
480
511
|
verify: verifyJson,
|
|
481
512
|
artifacts: [
|
|
482
|
-
"result.md", "result.json", "diff.patch", "git.diff", "test.log", "verify.log", "verify.json", "changed-files.json", "file-stats.json", "artifact_manifest.json",
|
|
513
|
+
"result.md", "result.json", "diff.patch", "git.diff", "test.log", "verify.log", "verify.json", "changed-files.json", "file-stats.json", "artifact_manifest.json", "post-task-cleanup.json",
|
|
483
514
|
...(outOfScopeChanges.length > 0 ? ["rollback_scope_violation_plan.md", "rollback-plan.json"] : []),
|
|
484
515
|
...(artifactStatus !== "collected" ? ["partial_result.md"] : []),
|
|
485
516
|
],
|
|
@@ -495,7 +526,7 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
495
526
|
failed_command: followup.failed_command,
|
|
496
527
|
suggested_next_action: followup.suggested_next_action,
|
|
497
528
|
safe_followup_prompt: followup.safe_followup_prompt,
|
|
498
|
-
next_steps: finalStatus === "
|
|
529
|
+
next_steps: finalStatus === "done_by_agent"
|
|
499
530
|
? ["Review get_task_summary and audit_task before accepting the work."]
|
|
500
531
|
: ["Resolve the reported failure before accepting the work."],
|
|
501
532
|
}, null, 2), "utf-8");
|
|
@@ -522,6 +553,7 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
522
553
|
artifact_collection_error: artifactCollectionError,
|
|
523
554
|
artifact_collection_started_at: artifactCollectionStartedAt,
|
|
524
555
|
artifact_collection_finished_at: artifactCollectionFinishedAt,
|
|
556
|
+
cleanup: cleanupReport,
|
|
525
557
|
out_of_scope_changes: outOfScopeChanges,
|
|
526
558
|
new_out_of_scope_changes: newOutOfScopeChanges,
|
|
527
559
|
preexisting_external_dirty_files: preexistingExternalDirty,
|
|
@@ -532,6 +564,7 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
532
564
|
workspace_dirty_before: changes.workspace_dirty_before,
|
|
533
565
|
workspace_dirty_after: changes.workspace_dirty_after,
|
|
534
566
|
workspace_dirty: changes.workspace_dirty_after,
|
|
567
|
+
acceptance_status: finalStatus === "done_by_agent" ? "pending" : null,
|
|
535
568
|
});
|
|
536
569
|
writeTaskRuntime(taskDir, {
|
|
537
570
|
phase: finalPhase,
|
|
@@ -559,7 +592,7 @@ function buildFailureFollowup(
|
|
|
559
592
|
safe_followup_prompt: string | null;
|
|
560
593
|
} {
|
|
561
594
|
const failed = commands.find((command) => command.status !== "passed");
|
|
562
|
-
if (status === "done") {
|
|
595
|
+
if (status === "done_by_agent" || status === "done") {
|
|
563
596
|
return {
|
|
564
597
|
failure_reason: null,
|
|
565
598
|
failed_command: null,
|
|
@@ -634,6 +667,12 @@ async function runManagedProcess(options: {
|
|
|
634
667
|
} catch (error) {
|
|
635
668
|
return { exitCode: null, stdout: "", stderr: "", spawnError: errorMessage(error), terminationReason: null };
|
|
636
669
|
}
|
|
670
|
+
// v0.7.0: record child_started_at immediately so diagnose_task can detect
|
|
671
|
+
// PID reuse by comparing this timestamp with the live process start time.
|
|
672
|
+
writeTaskRuntime(options.taskDir, {
|
|
673
|
+
child_pid: child.pid,
|
|
674
|
+
child_started_at: new Date().toISOString(),
|
|
675
|
+
});
|
|
637
676
|
|
|
638
677
|
const stdoutStream = openStream(options.stdoutPath);
|
|
639
678
|
const stderrStream = openStream(options.stderrPath);
|
|
@@ -904,7 +943,7 @@ function buildResultMarkdown(input: {
|
|
|
904
943
|
input.error || "Agent execution and configured verification completed successfully.",
|
|
905
944
|
"",
|
|
906
945
|
"## Risks",
|
|
907
|
-
input.status === "done"
|
|
946
|
+
input.status === "done_by_agent" || input.status === "done"
|
|
908
947
|
? "- Review git.diff and changed-files.json before accepting the task."
|
|
909
948
|
: "- Task did not complete successfully; outputs may be partial.",
|
|
910
949
|
"",
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.8.1: discovery token store — server-side token store for invoke_discovered_tool.
|
|
3
|
+
*
|
|
4
|
+
* discover_tools 为每个搜索结果生成 discoveryToken,server-side 保存 token 真实信息。
|
|
5
|
+
* invoke_discovered_tool 只接受 token id(不信任客户端回传的 token JSON)。
|
|
6
|
+
*
|
|
7
|
+
* 安全契约:
|
|
8
|
+
* - 不引入第三方 npm 依赖(用 node:crypto 的 randomBytes)
|
|
9
|
+
* - 不持久化到磁盘(内存 Map,进程内共享,单例)
|
|
10
|
+
* - token 单次使用语义(consumeToken 后即删除)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { randomBytes } from "node:crypto";
|
|
14
|
+
import { PatchWardenError } from "../errors.js";
|
|
15
|
+
import type { ToolRisk } from "../tools/toolRegistry.js";
|
|
16
|
+
import type { ToolProfile } from "../tools/toolCatalog.js";
|
|
17
|
+
|
|
18
|
+
// ── 类型定义 ──────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export interface DiscoveryTokenRecord {
|
|
21
|
+
token: string; // token id,格式 dst_{YYYYMMDD}_{randomHex12}
|
|
22
|
+
toolName: string; // 该 token 授权调用的工具名
|
|
23
|
+
risk: ToolRisk; // 该工具的风险等级
|
|
24
|
+
allowedScope?: string[]; // 允许的文件 scope(可选)
|
|
25
|
+
issuedAt: string; // ISO 时间戳
|
|
26
|
+
expiresAt: string; // ISO 时间戳,默认 issuedAt + 10 分钟
|
|
27
|
+
query: string; // discover_tools 时的查询词
|
|
28
|
+
schemaDigest: string; // 工具 inputSchema 的 sha256 digest
|
|
29
|
+
profile: ToolProfile; // discover 时的 profile
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface IssueTokenInput {
|
|
33
|
+
toolName: string;
|
|
34
|
+
risk: ToolRisk;
|
|
35
|
+
query: string;
|
|
36
|
+
schemaDigest: string;
|
|
37
|
+
profile: ToolProfile;
|
|
38
|
+
allowedScope?: string[];
|
|
39
|
+
ttlMs?: number; // 默认 10 * 60 * 1000(10 分钟)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── 常量 ──────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
const DEFAULT_TTL_MS = 10 * 60 * 1000; // 10 分钟
|
|
45
|
+
|
|
46
|
+
// ── 模块级 token store(单例,进程内共享) ───────────────────────
|
|
47
|
+
|
|
48
|
+
const tokenStore = new Map<string, DiscoveryTokenRecord>();
|
|
49
|
+
|
|
50
|
+
// ── 内部工具 ──────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
function formatDatePart(date: Date): string {
|
|
53
|
+
const y = date.getFullYear();
|
|
54
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
55
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
56
|
+
return `${y}${m}${d}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function generateTokenId(): string {
|
|
60
|
+
const datePart = formatDatePart(new Date());
|
|
61
|
+
const randomPart = randomBytes(6).toString("hex"); // 12 位 hex
|
|
62
|
+
return `dst_${datePart}_${randomPart}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── 公共 API ──────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 生成并保存 discovery token,返回 token id。
|
|
69
|
+
* token id 格式:dst_{YYYYMMDD}_{randomHex12}
|
|
70
|
+
*/
|
|
71
|
+
export function issueToken(input: IssueTokenInput): string {
|
|
72
|
+
const now = new Date();
|
|
73
|
+
const issuedAt = now.toISOString();
|
|
74
|
+
const expiresAt = new Date(Date.now() + (input.ttlMs ?? DEFAULT_TTL_MS)).toISOString();
|
|
75
|
+
const token = generateTokenId();
|
|
76
|
+
|
|
77
|
+
const record: DiscoveryTokenRecord = {
|
|
78
|
+
token,
|
|
79
|
+
toolName: input.toolName,
|
|
80
|
+
risk: input.risk,
|
|
81
|
+
allowedScope: input.allowedScope,
|
|
82
|
+
issuedAt,
|
|
83
|
+
expiresAt,
|
|
84
|
+
query: input.query,
|
|
85
|
+
schemaDigest: input.schemaDigest,
|
|
86
|
+
profile: input.profile,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
tokenStore.set(token, record);
|
|
90
|
+
return token;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 消费 token(单次使用语义)。
|
|
95
|
+
* - 不存在 → 抛 PatchWardenError("token_not_found")
|
|
96
|
+
* - 过期(expiresAt 早于现在)→ 抛 PatchWardenError("token_expired")
|
|
97
|
+
* - 有效 → 从 store 删除并返回 record
|
|
98
|
+
*/
|
|
99
|
+
export function consumeToken(tokenId: string): DiscoveryTokenRecord {
|
|
100
|
+
const record = tokenStore.get(tokenId);
|
|
101
|
+
if (!record) {
|
|
102
|
+
throw new PatchWardenError(
|
|
103
|
+
"token_not_found",
|
|
104
|
+
`Discovery token not found: ${tokenId}`,
|
|
105
|
+
"Call discover_tools to obtain a fresh discovery token before invoking invoke_discovered_tool.",
|
|
106
|
+
true,
|
|
107
|
+
{ token: tokenId }
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
const expiresAtMs = Date.parse(record.expiresAt);
|
|
113
|
+
if (expiresAtMs < now) {
|
|
114
|
+
// 过期 token 从 store 删除,避免累积
|
|
115
|
+
tokenStore.delete(tokenId);
|
|
116
|
+
throw new PatchWardenError(
|
|
117
|
+
"token_expired",
|
|
118
|
+
`Discovery token expired: ${tokenId}`,
|
|
119
|
+
"Call discover_tools again to obtain a fresh discovery token.",
|
|
120
|
+
true,
|
|
121
|
+
{ token: tokenId, expired_at: record.expiresAt }
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
tokenStore.delete(tokenId);
|
|
126
|
+
return record;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 只读查看 token,不消费、不校验过期。
|
|
131
|
+
* 不存在返回 null,存在返回 record(不删除)。
|
|
132
|
+
*/
|
|
133
|
+
export function peekToken(tokenId: string): DiscoveryTokenRecord | null {
|
|
134
|
+
const record = tokenStore.get(tokenId);
|
|
135
|
+
return record ?? null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 撤销 token,返回是否删除成功。
|
|
140
|
+
*/
|
|
141
|
+
export function revokeToken(tokenId: string): boolean {
|
|
142
|
+
return tokenStore.delete(tokenId);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 清空所有 token(测试用)。
|
|
147
|
+
*/
|
|
148
|
+
export function clearAllTokens(): void {
|
|
149
|
+
tokenStore.clear();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 返回当前 store 中的 token 数量(测试/诊断用)。
|
|
154
|
+
*/
|
|
155
|
+
export function getActiveTokenCount(): number {
|
|
156
|
+
return tokenStore.size;
|
|
157
|
+
}
|