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,168 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { getConfig } from "../config.js";
|
|
4
|
+
import { guardReadPath, guardWorkspacePath } from "../security/pathGuard.js";
|
|
5
|
+
import { redactSensitiveValue } from "../security/contentRedaction.js";
|
|
6
|
+
import { getProjectPolicySummary } from "../policy/projectPolicy.js";
|
|
7
|
+
import { getTaskLineage } from "./taskLineage.js";
|
|
8
|
+
import { getLastToolCatalogSnapshot } from "./toolCatalog.js";
|
|
9
|
+
import { PATCHWARDEN_VERSION, TOOL_SCHEMA_EPOCH } from "../version.js";
|
|
10
|
+
import { PatchWardenError } from "../errors.js";
|
|
11
|
+
export function exportTaskEvidencePack(input) {
|
|
12
|
+
const lineageId = normalizeLineageId(input.lineage_id);
|
|
13
|
+
const maxItems = normalizeMaxItems(input.max_items);
|
|
14
|
+
const config = getConfig();
|
|
15
|
+
const lineage = getTaskLineage(lineageId, { max_items: maxItems });
|
|
16
|
+
const packDir = resolve(config.workspaceRoot, ".patchwarden", "evidence-packs", lineageId);
|
|
17
|
+
guardWorkspacePath(packDir, config.workspaceRoot);
|
|
18
|
+
mkdirSync(packDir, { recursive: true });
|
|
19
|
+
const policySummary = readPolicySummary(lineage.repo_path);
|
|
20
|
+
const catalog = safeCatalog();
|
|
21
|
+
const generatedAt = new Date().toISOString();
|
|
22
|
+
const safePack = redactSensitiveValue({
|
|
23
|
+
evidence_pack_id: `evidence_${lineageId}`,
|
|
24
|
+
lineage_id: lineageId,
|
|
25
|
+
generated_at: generatedAt,
|
|
26
|
+
path: packDir,
|
|
27
|
+
files: {
|
|
28
|
+
json: join(packDir, "evidence.json"),
|
|
29
|
+
markdown: join(packDir, "EVIDENCE.md"),
|
|
30
|
+
},
|
|
31
|
+
lineage,
|
|
32
|
+
policy: {
|
|
33
|
+
valid: policySummary.valid,
|
|
34
|
+
issue_count: policySummary.issues.length,
|
|
35
|
+
release_readiness: policySummary.release_readiness,
|
|
36
|
+
},
|
|
37
|
+
catalog,
|
|
38
|
+
omitted: ["stdout", "stderr", "diff", "verification command logs", "full artifact markdown", "sensitive file contents"],
|
|
39
|
+
next_action: buildNextAction(lineage),
|
|
40
|
+
bounded: true,
|
|
41
|
+
}).value;
|
|
42
|
+
writeFileSync(safePack.files.json, JSON.stringify(safePack, null, 2) + "\n", "utf-8");
|
|
43
|
+
writeFileSync(safePack.files.markdown, buildEvidenceMarkdown(safePack), "utf-8");
|
|
44
|
+
return safePack;
|
|
45
|
+
}
|
|
46
|
+
export function listEvidencePacks(options = {}) {
|
|
47
|
+
const maxItems = normalizeMaxItems(options.max_items);
|
|
48
|
+
const config = getConfig();
|
|
49
|
+
const root = resolve(config.workspaceRoot, ".patchwarden", "evidence-packs");
|
|
50
|
+
guardWorkspacePath(root, config.workspaceRoot);
|
|
51
|
+
if (!existsSync(root))
|
|
52
|
+
return { evidence_packs: [], total: 0, truncated: false };
|
|
53
|
+
const packs = readdirSync(root, { withFileTypes: true })
|
|
54
|
+
.filter((entry) => entry.isDirectory())
|
|
55
|
+
.map((entry) => readEvidencePack(entry.name))
|
|
56
|
+
.filter((entry) => entry !== null)
|
|
57
|
+
.sort((a, b) => b.generated_at.localeCompare(a.generated_at));
|
|
58
|
+
return {
|
|
59
|
+
evidence_packs: packs.slice(0, maxItems),
|
|
60
|
+
total: packs.length,
|
|
61
|
+
truncated: packs.length > maxItems,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function readEvidencePack(lineageId) {
|
|
65
|
+
const safeLineageId = normalizeLineageId(lineageId);
|
|
66
|
+
const config = getConfig();
|
|
67
|
+
const filePath = resolve(config.workspaceRoot, ".patchwarden", "evidence-packs", safeLineageId, "evidence.json");
|
|
68
|
+
guardReadPath(filePath, config.workspaceRoot, ".patchwarden/evidence-packs");
|
|
69
|
+
if (!existsSync(filePath))
|
|
70
|
+
return null;
|
|
71
|
+
const raw = readFileSync(filePath, "utf-8").replace(/^\uFEFF/, "");
|
|
72
|
+
return redactSensitiveValue(JSON.parse(raw)).value;
|
|
73
|
+
}
|
|
74
|
+
function normalizeLineageId(value) {
|
|
75
|
+
const lineageId = String(value || "").trim();
|
|
76
|
+
if (!/^[A-Za-z0-9_-]+$/.test(lineageId)) {
|
|
77
|
+
throw new PatchWardenError("invalid_lineage_id", "lineage_id may contain only letters, numbers, underscores, and hyphens.", "Pass a lineage_id returned by run_task_loop.", true, { lineage_id: lineageId });
|
|
78
|
+
}
|
|
79
|
+
return lineageId;
|
|
80
|
+
}
|
|
81
|
+
function normalizeMaxItems(value) {
|
|
82
|
+
if (value === undefined)
|
|
83
|
+
return 12;
|
|
84
|
+
if (!Number.isInteger(value) || value < 1 || value > 50) {
|
|
85
|
+
throw new Error("max_items must be an integer from 1 to 50.");
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
function readPolicySummary(repoPath) {
|
|
90
|
+
try {
|
|
91
|
+
const policy = getProjectPolicySummary(repoPath || ".");
|
|
92
|
+
return {
|
|
93
|
+
valid: Boolean(policy.valid),
|
|
94
|
+
issues: Array.isArray(policy.issues) ? policy.issues : [],
|
|
95
|
+
release_readiness: policy.release_readiness || null,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
return {
|
|
100
|
+
valid: false,
|
|
101
|
+
issues: [{ code: "policy_unavailable", message: err instanceof Error ? err.message : String(err) }],
|
|
102
|
+
release_readiness: null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function safeCatalog() {
|
|
107
|
+
try {
|
|
108
|
+
const catalog = getLastToolCatalogSnapshot();
|
|
109
|
+
if (!catalog)
|
|
110
|
+
throw new Error("catalog snapshot unavailable");
|
|
111
|
+
return {
|
|
112
|
+
server_version: catalog.server_version,
|
|
113
|
+
schema_epoch: catalog.schema_epoch,
|
|
114
|
+
tool_profile: catalog.tool_profile,
|
|
115
|
+
tool_count: catalog.tool_count,
|
|
116
|
+
tool_manifest_sha256: catalog.tool_manifest_sha256,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return {
|
|
121
|
+
server_version: PATCHWARDEN_VERSION,
|
|
122
|
+
schema_epoch: TOOL_SCHEMA_EPOCH,
|
|
123
|
+
tool_profile: null,
|
|
124
|
+
tool_count: null,
|
|
125
|
+
tool_manifest_sha256: null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function buildNextAction(lineage) {
|
|
130
|
+
if (lineage.stop_reason === "success")
|
|
131
|
+
return "Use this evidence pack for review or release readiness.";
|
|
132
|
+
return lineage.next_action || "Review lineage before exporting release evidence.";
|
|
133
|
+
}
|
|
134
|
+
function buildEvidenceMarkdown(pack) {
|
|
135
|
+
const direct = pack.lineage.tasks.direct_sessions
|
|
136
|
+
.map((entry) => `- ${entry.session_id}: status=${entry.status || "unknown"}, audit=${entry.audit_decision || "unknown"}`)
|
|
137
|
+
.join("\n");
|
|
138
|
+
const rounds = pack.lineage.rounds
|
|
139
|
+
.map((round) => `- ${round.role} ${round.task_id}: status=${round.status}, verification=${round.verification_status}, audit=${round.audit_verdict}`)
|
|
140
|
+
.join("\n");
|
|
141
|
+
return [
|
|
142
|
+
"# PatchWarden Evidence Pack",
|
|
143
|
+
"",
|
|
144
|
+
`- Evidence pack: ${pack.evidence_pack_id}`,
|
|
145
|
+
`- Lineage: ${pack.lineage_id}`,
|
|
146
|
+
`- Generated: ${pack.generated_at}`,
|
|
147
|
+
`- Final status: ${pack.lineage.final_status}`,
|
|
148
|
+
`- Stop reason: ${pack.lineage.stop_reason}`,
|
|
149
|
+
`- Worktree: ${pack.lineage.worktree.isolation_mode} (${pack.lineage.worktree.status})`,
|
|
150
|
+
`- Tool manifest: ${pack.catalog.tool_manifest_sha256 || "unavailable"}`,
|
|
151
|
+
"",
|
|
152
|
+
"## Verification Rounds",
|
|
153
|
+
rounds || "- None.",
|
|
154
|
+
"",
|
|
155
|
+
"## Direct Evidence",
|
|
156
|
+
direct || "- None.",
|
|
157
|
+
"",
|
|
158
|
+
"## Policy And Release",
|
|
159
|
+
`- Policy valid: ${pack.policy.valid}`,
|
|
160
|
+
`- Policy issue count: ${pack.policy.issue_count}`,
|
|
161
|
+
"",
|
|
162
|
+
"## Omitted",
|
|
163
|
+
...pack.omitted.map((item) => `- ${item}`),
|
|
164
|
+
"",
|
|
165
|
+
`Next action: ${pack.next_action}`,
|
|
166
|
+
"",
|
|
167
|
+
].join("\n");
|
|
168
|
+
}
|
|
@@ -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;
|
|
@@ -0,0 +1,110 @@
|
|
|
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 { readGoalStatus, writeGoalStatus } from "../goal/goalStore.js";
|
|
17
|
+
import { addSubgoal, linkTaskToSubgoal, updateSubgoalStatus } from "../goal/goalStatus.js";
|
|
18
|
+
import { createTask } from "./createTask.js";
|
|
19
|
+
import { createWorktree, discardWorktree } from "../goal/worktreeManager.js";
|
|
20
|
+
import { getConfig } from "../config.js";
|
|
21
|
+
import { PatchWardenError } from "../errors.js";
|
|
22
|
+
// ── 函数实现 ──────────────────────────────────────────────────────
|
|
23
|
+
/**
|
|
24
|
+
* 原子地创建一个 subgoal 并关联到一个新任务。
|
|
25
|
+
*
|
|
26
|
+
* @throws PatchWardenError("goal_not_found") 当 goal_id 不存在
|
|
27
|
+
* @throws PatchWardenError("invalid_dependency") 当 depends_on 引用不存在的 subgoal
|
|
28
|
+
* @throws PatchWardenError("invalid_execution_mode") 当 execution_mode 为 "assess_only"
|
|
29
|
+
*/
|
|
30
|
+
export function createSubgoalTask(input) {
|
|
31
|
+
// assess_only 不产生 task,无法关联到 subgoal
|
|
32
|
+
if (input.execution_mode === "assess_only") {
|
|
33
|
+
throw new PatchWardenError("invalid_execution_mode", "createSubgoalTask does not support execution_mode \"assess_only\"", "Use execution_mode \"execute\" (default) so a task is created and linked to the subgoal.", true, { goal_id: input.goal_id, execution_mode: input.execution_mode });
|
|
34
|
+
}
|
|
35
|
+
// 1. 读取 goal_status.json,如果不存在抛 goal_not_found
|
|
36
|
+
let goalStatus;
|
|
37
|
+
try {
|
|
38
|
+
goalStatus = readGoalStatus(input.goal_id);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new PatchWardenError("goal_not_found", `Goal "${input.goal_id}" not found`, "Call list_goals to see available goals.", true, { goal_id: input.goal_id });
|
|
42
|
+
}
|
|
43
|
+
// 2. addSubgoal(校验 depends_on)
|
|
44
|
+
const { goalStatus: withSubgoal, subgoalId } = addSubgoal(goalStatus, input.subgoal_title, input.depends_on ?? []);
|
|
45
|
+
// 2.5 Worktree 隔离(默认开启):为该 subgoal task 创建独立 git worktree,
|
|
46
|
+
// 用 worktree 路径作为 createTask 的 repo_path,避免并发 task 互相污染。
|
|
47
|
+
// isolate_worktree === false 时退化为原行为(使用 input.repo_path)。
|
|
48
|
+
const isolate = input.isolate_worktree !== false;
|
|
49
|
+
let repoPathForTask = input.repo_path;
|
|
50
|
+
let worktreeId = null;
|
|
51
|
+
if (isolate) {
|
|
52
|
+
const workspaceRoot = getConfig().workspaceRoot;
|
|
53
|
+
const wt = createWorktree(input.goal_id, subgoalId, workspaceRoot);
|
|
54
|
+
repoPathForTask = wt.worktreePath;
|
|
55
|
+
worktreeId = wt.worktreeId;
|
|
56
|
+
}
|
|
57
|
+
// 3. 调用 createTask 创建关联任务(强制 execute 模式,确保返回 CreateTaskOutput)
|
|
58
|
+
let taskId;
|
|
59
|
+
try {
|
|
60
|
+
const taskResult = createTask({
|
|
61
|
+
plan_id: input.plan_id,
|
|
62
|
+
inline_plan: input.inline_plan,
|
|
63
|
+
plan_title: input.plan_title,
|
|
64
|
+
template: input.template,
|
|
65
|
+
goal: input.goal,
|
|
66
|
+
agent: input.agent,
|
|
67
|
+
repo_path: repoPathForTask,
|
|
68
|
+
test_command: input.test_command,
|
|
69
|
+
verify_commands: input.verify_commands,
|
|
70
|
+
timeout_seconds: input.timeout_seconds,
|
|
71
|
+
execution_mode: "execute",
|
|
72
|
+
assessment_id: input.assessment_id,
|
|
73
|
+
scope: input.scope,
|
|
74
|
+
forbidden: input.forbidden,
|
|
75
|
+
verification: input.verification,
|
|
76
|
+
done_evidence: input.done_evidence,
|
|
77
|
+
goal_id: input.goal_id,
|
|
78
|
+
subgoal_id: subgoalId,
|
|
79
|
+
});
|
|
80
|
+
// execution_mode 为 "execute",createTask 返回 CreateTaskOutput(含 task_id)
|
|
81
|
+
taskId = taskResult.task_id;
|
|
82
|
+
}
|
|
83
|
+
catch (createTaskErr) {
|
|
84
|
+
// createTask 失败:清理已创建的隔离 worktree,避免遗留半成品(best effort)
|
|
85
|
+
if (isolate && worktreeId) {
|
|
86
|
+
try {
|
|
87
|
+
discardWorktree(worktreeId, getConfig().workspaceRoot);
|
|
88
|
+
}
|
|
89
|
+
catch { /* ignore cleanup failure,向上抛出原始 createTask 错误 */ }
|
|
90
|
+
}
|
|
91
|
+
throw createTaskErr;
|
|
92
|
+
}
|
|
93
|
+
// 4. linkTaskToSubgoal + updateSubgoalStatus(running)
|
|
94
|
+
const withTask = linkTaskToSubgoal(withSubgoal, subgoalId, taskId);
|
|
95
|
+
let withRunning = updateSubgoalStatus(withTask, subgoalId, "running");
|
|
96
|
+
// 4.5 隔离模式下,把 worktree_id 记录到 subgoal 对象(写入 goal_status.json)
|
|
97
|
+
if (isolate && worktreeId) {
|
|
98
|
+
const newSubgoals = withRunning.subgoals.map((s) => s.id === subgoalId
|
|
99
|
+
? { ...s, worktree_id: worktreeId }
|
|
100
|
+
: s);
|
|
101
|
+
withRunning = { ...withRunning, subgoals: newSubgoals };
|
|
102
|
+
}
|
|
103
|
+
// 5. 写回 goal_status.json
|
|
104
|
+
writeGoalStatus(input.goal_id, withRunning);
|
|
105
|
+
return {
|
|
106
|
+
subgoal_id: subgoalId,
|
|
107
|
+
task_id: taskId,
|
|
108
|
+
subgoal_status: "running",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -5,7 +5,7 @@ import { getConfig, getTasksDir, getDirectSessionsDir, resolveWorkspaceRoot } fr
|
|
|
5
5
|
import { listAgents } from "./listAgents.js";
|
|
6
6
|
import { redactSensitiveContent, redactSensitiveValue } from "../security/contentRedaction.js";
|
|
7
7
|
import { PATCHWARDEN_VERSION, TOOL_SCHEMA_EPOCH } from "../version.js";
|
|
8
|
-
import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, resolveToolProfile } from "./toolCatalog.js";
|
|
8
|
+
import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, CHATGPT_SEARCH_TOOL_NAMES, resolveToolProfile } from "./toolCatalog.js";
|
|
9
9
|
import { readWatcherStatus } from "../watcherStatus.js";
|
|
10
10
|
import { listTasks } from "./listTasks.js";
|
|
11
11
|
const SERVER_STARTED_AT = Date.now();
|
|
@@ -39,6 +39,9 @@ export function healthCheck(catalog, input = {}) {
|
|
|
39
39
|
profileErrors.push("Direct profile is disabled (enableDirectProfile=false). Only health_check is available. Set enableDirectProfile: true to enable Direct session tools.");
|
|
40
40
|
profileConsistent = false;
|
|
41
41
|
}
|
|
42
|
+
else if (activeProfile === "chatgpt_search") {
|
|
43
|
+
expectedNames = [...CHATGPT_SEARCH_TOOL_NAMES];
|
|
44
|
+
}
|
|
42
45
|
if (expectedNames) {
|
|
43
46
|
const catalogNames = new Set(catalog.tool_names);
|
|
44
47
|
for (const name of expectedNames) {
|
|
@@ -95,6 +98,7 @@ export function healthCheck(catalog, input = {}) {
|
|
|
95
98
|
direct_sessions_dir: directSessions,
|
|
96
99
|
direct_session_ttl_seconds: config.directSessionTtlSeconds,
|
|
97
100
|
...(activeProfile === "chatgpt_direct" ? { direct_tool_count: config.enableDirectProfile ? CHATGPT_DIRECT_TOOL_NAMES.length : 1 } : {}),
|
|
101
|
+
...(activeProfile === "chatgpt_search" ? { search_tool_count: CHATGPT_SEARCH_TOOL_NAMES.length } : {}),
|
|
98
102
|
connector_visibility: {
|
|
99
103
|
status: "not_observable_server_side",
|
|
100
104
|
verification: "Refresh or reconnect the Connector and verify tools/list from a new ChatGPT conversation.",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.8.1: invoke_discovered_tool MCP 工具封装层。
|
|
3
|
+
*
|
|
4
|
+
* 受控动态调用链:agent 必须先 discover_tools 获取 discoveryToken,
|
|
5
|
+
* 再凭 token 调用工具。本模块串联 consumeToken → toolMeta 查找 →
|
|
6
|
+
* 防递归 → checkInvocation → dispatch → logToolInvocation,
|
|
7
|
+
* 任一环节失败都记审计日志并返回错误结果,不向上层抛错。
|
|
8
|
+
*/
|
|
9
|
+
import type { ToolProfile } from "./toolCatalog.js";
|
|
10
|
+
import type { ToolDef } from "./registry.js";
|
|
11
|
+
export interface InvokeDiscoveredToolInput {
|
|
12
|
+
toolName: string;
|
|
13
|
+
arguments: Record<string, unknown>;
|
|
14
|
+
discoveryToken: string;
|
|
15
|
+
assessmentId?: string;
|
|
16
|
+
}
|
|
17
|
+
export type ToolDispatch = (name: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
18
|
+
export interface InvokeDiscoveredToolContext {
|
|
19
|
+
tools: ToolDef[];
|
|
20
|
+
profile: ToolProfile;
|
|
21
|
+
dispatch: ToolDispatch;
|
|
22
|
+
}
|
|
23
|
+
export interface InvokeDiscoveredToolResult {
|
|
24
|
+
ok: boolean;
|
|
25
|
+
result?: unknown;
|
|
26
|
+
error?: {
|
|
27
|
+
reason: string;
|
|
28
|
+
message: string;
|
|
29
|
+
};
|
|
30
|
+
invocation_log_written: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 受控动态调用入口。串联 token 消费、toolMeta 查找、防递归、
|
|
34
|
+
* 8 项守卫校验、工具分发和审计日志。任何环节失败都记日志并返回
|
|
35
|
+
* 错误结果,不向上层抛错。
|
|
36
|
+
*/
|
|
37
|
+
export declare function invokeDiscoveredTool(input: InvokeDiscoveredToolInput, context: InvokeDiscoveredToolContext): Promise<InvokeDiscoveredToolResult>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.8.1: invoke_discovered_tool MCP 工具封装层。
|
|
3
|
+
*
|
|
4
|
+
* 受控动态调用链:agent 必须先 discover_tools 获取 discoveryToken,
|
|
5
|
+
* 再凭 token 调用工具。本模块串联 consumeToken → toolMeta 查找 →
|
|
6
|
+
* 防递归 → checkInvocation → dispatch → logToolInvocation,
|
|
7
|
+
* 任一环节失败都记审计日志并返回错误结果,不向上层抛错。
|
|
8
|
+
*/
|
|
9
|
+
import { consumeToken } from "../security/discoveryTokenStore.js";
|
|
10
|
+
import { checkInvocation } from "../security/toolInvocationGuard.js";
|
|
11
|
+
import { logToolInvocation, computeArgumentsDigest } from "../logging.js";
|
|
12
|
+
import { buildToolRegistry } from "./toolRegistry.js";
|
|
13
|
+
import { PatchWardenError } from "../errors.js";
|
|
14
|
+
// ── 内部错误码常量 ────────────────────────────────────────────────
|
|
15
|
+
const RECURSIVE_INVOCATION_BLOCKED = "recursive_invocation_blocked";
|
|
16
|
+
const TOOL_NOT_REGISTERED = "tool_not_registered";
|
|
17
|
+
const DISPATCH_ERROR = "dispatch_error";
|
|
18
|
+
// ── 公共 API ──────────────────────────────────────────────────────
|
|
19
|
+
/**
|
|
20
|
+
* 受控动态调用入口。串联 token 消费、toolMeta 查找、防递归、
|
|
21
|
+
* 8 项守卫校验、工具分发和审计日志。任何环节失败都记日志并返回
|
|
22
|
+
* 错误结果,不向上层抛错。
|
|
23
|
+
*/
|
|
24
|
+
export async function invokeDiscoveredTool(input, context) {
|
|
25
|
+
const startTime = Date.now();
|
|
26
|
+
const argumentsDigest = computeArgumentsDigest(input.arguments);
|
|
27
|
+
let tokenRecord = null;
|
|
28
|
+
let toolMeta = null;
|
|
29
|
+
const writeLog = (result, errorCode) => {
|
|
30
|
+
const durationMs = Date.now() - startTime;
|
|
31
|
+
logToolInvocation({
|
|
32
|
+
timestamp: new Date().toISOString(),
|
|
33
|
+
toolName: input.toolName,
|
|
34
|
+
discoveryToken: input.discoveryToken,
|
|
35
|
+
risk: toolMeta?.risk ?? "unknown",
|
|
36
|
+
profile: context.profile,
|
|
37
|
+
arguments_digest: argumentsDigest,
|
|
38
|
+
...(tokenRecord?.allowedScope ? { allowedScope: tokenRecord.allowedScope } : {}),
|
|
39
|
+
result,
|
|
40
|
+
...(errorCode ? { error_code: errorCode } : {}),
|
|
41
|
+
duration_ms: durationMs,
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
const fail = (reason, message) => {
|
|
45
|
+
writeLog("error", reason);
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
error: { reason, message },
|
|
49
|
+
invocation_log_written: true,
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
try {
|
|
53
|
+
// 1. consumeToken(单次使用语义,token_not_found / token_expired 在此抛出)
|
|
54
|
+
try {
|
|
55
|
+
tokenRecord = consumeToken(input.discoveryToken);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
if (err instanceof PatchWardenError) {
|
|
59
|
+
return fail(err.reason, err.message);
|
|
60
|
+
}
|
|
61
|
+
return fail("token_error", err instanceof Error ? err.message : String(err));
|
|
62
|
+
}
|
|
63
|
+
// 2. 查 toolMeta(防止调用未注册工具)
|
|
64
|
+
const registry = buildToolRegistry(context.tools);
|
|
65
|
+
toolMeta = registry.find((meta) => meta.name === input.toolName) ?? null;
|
|
66
|
+
if (!toolMeta) {
|
|
67
|
+
return fail(TOOL_NOT_REGISTERED, `Tool "${input.toolName}" is not registered in the tool registry.`);
|
|
68
|
+
}
|
|
69
|
+
// 3. 防递归:禁止调用 invoke_discovered_tool 自身(防止绕过守卫)
|
|
70
|
+
if (input.toolName === "invoke_discovered_tool") {
|
|
71
|
+
return fail(RECURSIVE_INVOCATION_BLOCKED, "Recursive invocation of invoke_discovered_tool is blocked. invoke_discovered_tool cannot call itself.");
|
|
72
|
+
}
|
|
73
|
+
// 4. checkInvocation(8 项调用前强制校验)
|
|
74
|
+
try {
|
|
75
|
+
checkInvocation({
|
|
76
|
+
toolName: input.toolName,
|
|
77
|
+
toolMeta,
|
|
78
|
+
args: input.arguments,
|
|
79
|
+
discoveryTokenRecord: tokenRecord,
|
|
80
|
+
profile: context.profile,
|
|
81
|
+
assessmentId: input.assessmentId,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
if (err instanceof PatchWardenError) {
|
|
86
|
+
return fail(err.reason, err.message);
|
|
87
|
+
}
|
|
88
|
+
return fail("guard_error", err instanceof Error ? err.message : String(err));
|
|
89
|
+
}
|
|
90
|
+
// 5. dispatch(调用实际工具 handler)
|
|
91
|
+
let dispatchResult;
|
|
92
|
+
try {
|
|
93
|
+
dispatchResult = await context.dispatch(input.toolName, input.arguments);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
97
|
+
return fail(DISPATCH_ERROR, `Dispatch failed for tool "${input.toolName}": ${message}`);
|
|
98
|
+
}
|
|
99
|
+
// 6. 成功 → 记审计日志 → 返回成功结果
|
|
100
|
+
writeLog("ok");
|
|
101
|
+
return {
|
|
102
|
+
ok: true,
|
|
103
|
+
result: dispatchResult,
|
|
104
|
+
invocation_log_written: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
// 兜底:任何意外错误都记日志并返回错误结果(不抛给上层)
|
|
109
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
110
|
+
return fail("unexpected_error", `Unexpected error during invoke_discovered_tool: ${message}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TaskPhase, TaskStatus } from "./createTask.js";
|
|
1
|
+
import type { TaskPhase, TaskStatus, AcceptanceStatus } from "./createTask.js";
|
|
2
2
|
import { type PendingReason, type WatcherState, type WatcherStatusSnapshot } from "../watcherStatus.js";
|
|
3
3
|
export interface TaskEntry {
|
|
4
4
|
task_id: string;
|
|
@@ -7,6 +7,7 @@ export interface TaskEntry {
|
|
|
7
7
|
agent: string;
|
|
8
8
|
status: TaskStatus;
|
|
9
9
|
phase: TaskPhase;
|
|
10
|
+
acceptance_status: AcceptanceStatus;
|
|
10
11
|
created_at: string;
|
|
11
12
|
updated_at: string;
|
|
12
13
|
workspace_root: string;
|
|
@@ -25,6 +26,7 @@ export interface ListTasksInput {
|
|
|
25
26
|
status?: string;
|
|
26
27
|
repo_path?: string;
|
|
27
28
|
active_only?: boolean;
|
|
29
|
+
acceptance_status?: string;
|
|
28
30
|
limit?: number;
|
|
29
31
|
}
|
|
30
32
|
export interface ListTasksOutput {
|
package/dist/tools/listTasks.js
CHANGED
|
@@ -9,6 +9,7 @@ export function listTasks(input) {
|
|
|
9
9
|
const plansDir = getPlansDir(config);
|
|
10
10
|
const limit = input?.limit && input.limit > 0 ? Math.min(input.limit, 100) : 20;
|
|
11
11
|
const filterStatus = input?.status || null;
|
|
12
|
+
const filterAcceptance = input?.acceptance_status || null;
|
|
12
13
|
const filterRepo = input?.repo_path?.trim().replace(/\\/g, "/") || null;
|
|
13
14
|
const watcher = readWatcherStatus(config);
|
|
14
15
|
if (!existsSync(tasksDir)) {
|
|
@@ -40,7 +41,14 @@ export function listTasks(input) {
|
|
|
40
41
|
const runtime = readTaskRuntime(taskDir);
|
|
41
42
|
if (filterStatus && data.status !== filterStatus)
|
|
42
43
|
continue;
|
|
43
|
-
if (
|
|
44
|
+
if (filterAcceptance) {
|
|
45
|
+
const taskAcceptance = data.status === "done_by_agent"
|
|
46
|
+
? (typeof data.acceptance_status === "string" ? data.acceptance_status : "pending")
|
|
47
|
+
: null;
|
|
48
|
+
if (taskAcceptance !== filterAcceptance)
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (input?.active_only && !["pending", "running", "collecting_artifacts"].includes(String(data.status)))
|
|
44
52
|
continue;
|
|
45
53
|
const normalizedRepo = String(data.repo_path || ".").replace(/\\/g, "/");
|
|
46
54
|
const normalizedResolvedRepo = String(data.resolved_repo_path || "").replace(/\\/g, "/");
|
|
@@ -64,6 +72,10 @@ export function listTasks(input) {
|
|
|
64
72
|
}
|
|
65
73
|
}
|
|
66
74
|
const phase = runtime.phase || data.phase || "queued";
|
|
75
|
+
const VALID_ACCEPTANCE = ["pending", "accepted", "rejected", "needs_fix", "blocked"];
|
|
76
|
+
const acceptanceStatus = data.status === "done_by_agent"
|
|
77
|
+
? (typeof data.acceptance_status === "string" && VALID_ACCEPTANCE.includes(data.acceptance_status) ? data.acceptance_status : "pending")
|
|
78
|
+
: null;
|
|
67
79
|
tasks.push({
|
|
68
80
|
task_id: taskId,
|
|
69
81
|
plan_id: data.plan_id || "",
|
|
@@ -71,6 +83,7 @@ export function listTasks(input) {
|
|
|
71
83
|
agent: data.agent || "",
|
|
72
84
|
status: data.status || "pending",
|
|
73
85
|
phase,
|
|
86
|
+
acceptance_status: acceptanceStatus,
|
|
74
87
|
created_at: data.created_at || "",
|
|
75
88
|
updated_at: data.updated_at || "",
|
|
76
89
|
workspace_root: data.workspace_root || config.workspaceRoot,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v1.0.0 Part B: merge_worktree MCP 工具 handler — 把隔离 worktree 的 branch
|
|
3
|
+
* 合并回主工作区,并将 worktree 状态置为 "merged"。
|
|
4
|
+
*
|
|
5
|
+
* 参考 src/tools/auditTask.ts 的导出/返回风格。
|
|
6
|
+
*/
|
|
7
|
+
export interface MergeWorktreeInput {
|
|
8
|
+
worktree_id: string;
|
|
9
|
+
repo_path: string;
|
|
10
|
+
}
|
|
11
|
+
export interface MergeWorktreeOutput {
|
|
12
|
+
worktree_id: string;
|
|
13
|
+
status: "merged";
|
|
14
|
+
merged_at: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 合并指定 worktree 的 branch 回 repo_path(主工作区)。
|
|
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_merge_failed") 当 git merge 失败
|
|
23
|
+
*/
|
|
24
|
+
export declare function mergeWorktreeTool(input: MergeWorktreeInput): MergeWorktreeOutput;
|