patchwarden 0.4.0 → 0.6.1
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.cmd +51 -0
- package/README.en.md +1022 -0
- package/README.md +897 -358
- package/dist/assessments/agentAssessor.d.ts +15 -0
- package/dist/assessments/agentAssessor.js +293 -0
- package/dist/assessments/assessmentStore.d.ts +133 -0
- package/dist/assessments/assessmentStore.js +238 -0
- package/dist/assessments/confirmCli.d.ts +9 -0
- package/dist/assessments/confirmCli.js +26 -0
- package/dist/config.d.ts +22 -1
- package/dist/config.js +156 -3
- package/dist/direct/directAudit.d.ts +23 -0
- package/dist/direct/directAudit.js +309 -0
- package/dist/direct/directGuards.d.ts +20 -0
- package/dist/direct/directGuards.js +137 -0
- package/dist/direct/directPatch.d.ts +17 -0
- package/dist/direct/directPatch.js +113 -0
- package/dist/direct/directSessionStore.d.ts +63 -0
- package/dist/direct/directSessionStore.js +192 -0
- package/dist/direct/directVerification.d.ts +12 -0
- package/dist/direct/directVerification.js +96 -0
- package/dist/doctor.js +73 -5
- package/dist/logging.d.ts +52 -0
- package/dist/logging.js +123 -0
- package/dist/runner/agentInvocation.d.ts +23 -0
- package/dist/runner/agentInvocation.js +106 -0
- package/dist/runner/changeCapture.d.ts +70 -0
- package/dist/runner/changeCapture.js +278 -6
- package/dist/runner/runTask.js +271 -65
- package/dist/runner/simpleProcess.d.ts +21 -0
- package/dist/runner/simpleProcess.js +206 -0
- package/dist/runner/watch.js +17 -2
- package/dist/security/commandGuard.d.ts +2 -1
- package/dist/security/commandGuard.js +25 -3
- package/dist/security/riskEngine.d.ts +27 -0
- package/dist/security/riskEngine.js +118 -0
- package/dist/security/runtimeGuard.d.ts +6 -0
- package/dist/security/runtimeGuard.js +28 -0
- package/dist/smoke-test.js +1353 -7
- package/dist/test/unit/android-doctor.test.d.ts +1 -0
- package/dist/test/unit/android-doctor.test.js +118 -0
- package/dist/test/unit/chinese-path.test.d.ts +1 -0
- package/dist/test/unit/chinese-path.test.js +91 -0
- package/dist/test/unit/command-guard.test.d.ts +1 -0
- package/dist/test/unit/command-guard.test.js +160 -0
- package/dist/test/unit/direct-guards.test.d.ts +1 -0
- package/dist/test/unit/direct-guards.test.js +213 -0
- package/dist/test/unit/logging.test.d.ts +1 -0
- package/dist/test/unit/logging.test.js +275 -0
- package/dist/test/unit/path-guard.test.d.ts +1 -0
- package/dist/test/unit/path-guard.test.js +109 -0
- package/dist/test/unit/safe-status.test.d.ts +1 -0
- package/dist/test/unit/safe-status.test.js +165 -0
- package/dist/test/unit/sensitive-guard.test.d.ts +1 -0
- package/dist/test/unit/sensitive-guard.test.js +104 -0
- package/dist/test/unit/sync-file.test.d.ts +1 -0
- package/dist/test/unit/sync-file.test.js +154 -0
- package/dist/test/unit/watcher-status.test.d.ts +1 -0
- package/dist/test/unit/watcher-status.test.js +169 -0
- package/dist/tools/androidDoctor.d.ts +38 -0
- package/dist/tools/androidDoctor.js +391 -0
- package/dist/tools/applyPatch.d.ts +16 -0
- package/dist/tools/applyPatch.js +41 -0
- package/dist/tools/auditSession.d.ts +5 -0
- package/dist/tools/auditSession.js +12 -0
- package/dist/tools/auditTask.d.ts +7 -0
- package/dist/tools/auditTask.js +105 -7
- package/dist/tools/createDirectSession.d.ts +14 -0
- package/dist/tools/createDirectSession.js +54 -0
- package/dist/tools/createTask.d.ts +48 -1
- package/dist/tools/createTask.js +298 -47
- package/dist/tools/finalizeDirectSession.d.ts +19 -0
- package/dist/tools/finalizeDirectSession.js +84 -0
- package/dist/tools/getTaskSummary.d.ts +50 -0
- package/dist/tools/getTaskSummary.js +79 -2
- package/dist/tools/healthCheck.d.ts +27 -12
- package/dist/tools/healthCheck.js +44 -6
- package/dist/tools/readWorkspaceFile.d.ts +7 -1
- package/dist/tools/readWorkspaceFile.js +48 -2
- package/dist/tools/registry.js +270 -19
- package/dist/tools/runVerification.d.ts +16 -0
- package/dist/tools/runVerification.js +32 -0
- package/dist/tools/safeStatus.d.ts +19 -0
- package/dist/tools/safeStatus.js +72 -0
- package/dist/tools/savePlan.d.ts +1 -0
- package/dist/tools/savePlan.js +38 -7
- package/dist/tools/searchWorkspace.d.ts +19 -0
- package/dist/tools/searchWorkspace.js +205 -0
- package/dist/tools/syncFile.d.ts +18 -0
- package/dist/tools/syncFile.js +65 -0
- package/dist/tools/taskOutputs.d.ts +2 -2
- package/dist/tools/taskTemplates.js +2 -1
- package/dist/tools/toolCatalog.d.ts +4 -3
- package/dist/tools/toolCatalog.js +34 -5
- package/dist/tools/waitForTask.d.ts +2 -2
- package/dist/tools/waitForTask.js +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watcherStatus.d.ts +1 -0
- package/dist/watcherStatus.js +96 -4
- package/docs/chatgpt-usage.md +101 -0
- package/docs/performance-notes.md +55 -0
- package/docs/release-checklist.md +14 -0
- package/docs/release-v0.6.0.md +71 -0
- package/docs/release-v0.6.1.md +75 -0
- package/examples/config.example.json +5 -0
- package/examples/openai-tunnel/README.md +11 -4
- package/examples/openai-tunnel/chatgpt-test-prompt.md +18 -14
- package/examples/openai-tunnel/tunnel-client.example.yaml +8 -0
- package/package.json +13 -8
- package/scripts/brand-check.js +58 -12
- package/scripts/control-smoke.js +206 -0
- package/scripts/http-mcp-smoke.js +10 -2
- package/scripts/launchers/Check-PatchWarden-Health.cmd +6 -0
- package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +6 -0
- package/scripts/launchers/Restart-PatchWarden.cmd +6 -0
- package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +7 -0
- package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +7 -0
- package/scripts/lifecycle-smoke.js +402 -8
- package/scripts/manage-patchwarden.ps1 +639 -0
- package/scripts/mcp-manifest-check.js +123 -58
- package/scripts/mcp-smoke.js +160 -2
- package/scripts/pack-clean.js +160 -5
- package/scripts/package-manifest-check.js +78 -0
- package/scripts/patchwarden-mcp-direct.cmd +7 -0
- package/scripts/patchwarden-mcp-stdio.cmd +1 -1
- package/scripts/restart-patchwarden.ps1 +5 -4
- package/scripts/start-patchwarden-tunnel.ps1 +261 -30
- package/scripts/tunnel-supervisor-smoke.js +36 -4
- package/scripts/unit-tests.js +36 -0
- package/scripts/watcher-supervisor-smoke.js +10 -6
- package/src/assessments/agentAssessor.ts +324 -0
- package/src/assessments/assessmentStore.ts +426 -0
- package/src/assessments/confirmCli.ts +29 -0
- package/src/config.ts +178 -4
- package/src/direct/directAudit.ts +400 -0
- package/src/direct/directGuards.ts +279 -0
- package/src/direct/directPatch.ts +258 -0
- package/src/direct/directSessionStore.ts +345 -0
- package/src/direct/directVerification.ts +138 -0
- package/src/doctor.ts +103 -7
- package/src/logging.ts +152 -0
- package/src/runner/agentInvocation.ts +125 -0
- package/src/runner/changeCapture.ts +352 -6
- package/src/runner/runTask.ts +279 -63
- package/src/runner/simpleProcess.ts +223 -0
- package/src/runner/watch.ts +18 -2
- package/src/security/commandGuard.ts +46 -4
- package/src/security/riskEngine.ts +160 -0
- package/src/security/runtimeGuard.ts +41 -0
- package/src/smoke-test.ts +1291 -4
- package/src/test/unit/android-doctor.test.ts +158 -0
- package/src/test/unit/chinese-path.test.ts +106 -0
- package/src/test/unit/command-guard.test.ts +221 -0
- package/src/test/unit/direct-guards.test.ts +297 -0
- package/src/test/unit/logging.test.ts +325 -0
- package/src/test/unit/path-guard.test.ts +150 -0
- package/src/test/unit/safe-status.test.ts +187 -0
- package/src/test/unit/sensitive-guard.test.ts +124 -0
- package/src/test/unit/sync-file.test.ts +231 -0
- package/src/test/unit/watcher-status.test.ts +190 -0
- package/src/tools/androidDoctor.ts +424 -0
- package/src/tools/applyPatch.ts +86 -0
- package/src/tools/auditSession.ts +28 -0
- package/src/tools/auditTask.ts +111 -7
- package/src/tools/createDirectSession.ts +113 -0
- package/src/tools/createTask.ts +405 -55
- package/src/tools/finalizeDirectSession.ts +144 -0
- package/src/tools/getTaskSummary.ts +133 -2
- package/src/tools/healthCheck.ts +45 -6
- package/src/tools/readWorkspaceFile.ts +85 -2
- package/src/tools/registry.ts +305 -19
- package/src/tools/runVerification.ts +58 -0
- package/src/tools/safeStatus.ts +96 -0
- package/src/tools/savePlan.ts +57 -7
- package/src/tools/searchWorkspace.ts +275 -0
- package/src/tools/syncFile.ts +122 -0
- package/src/tools/taskTemplates.ts +2 -1
- package/src/tools/toolCatalog.ts +37 -6
- package/src/tools/waitForTask.ts +3 -3
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +101 -4
- package/tsconfig.json +18 -17
- package/Check-PatchWarden-Health.cmd +0 -6
- package/Reset-PatchWarden-Tunnel-Key.cmd +0 -6
- package/Restart-PatchWarden.cmd +0 -19
- package/Start-PatchWarden-Tunnel.cmd +0 -7
- package/docs/release-v0.3.0.md +0 -43
- package/docs/release-v0.4.0.md +0 -74
package/dist/tools/createTask.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync, existsSync, statSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { mkdirSync, writeFileSync, existsSync, statSync, readFileSync } from "node:fs";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { resolve, join } from "node:path";
|
|
4
4
|
import { getTasksDir, getPlansDir, getConfig } from "../config.js";
|
|
5
5
|
import { guardPath, guardWorkspacePath, guardReadPath } from "../security/pathGuard.js";
|
|
6
6
|
import { guardTestCommand } from "../security/commandGuard.js";
|
|
7
|
+
import { guardRuntimeSelfModification } from "../security/runtimeGuard.js";
|
|
8
|
+
import { guardPlanContent } from "../security/planGuard.js";
|
|
9
|
+
import { assessRisk } from "../security/riskEngine.js";
|
|
10
|
+
import { createAssessment, readAssessment, validateAssessmentFreshness, generateAssessmentId, createAssessmentDir, } from "../assessments/assessmentStore.js";
|
|
11
|
+
import { runAgentAssessment } from "../assessments/agentAssessor.js";
|
|
12
|
+
import { captureRepoSnapshot } from "../runner/changeCapture.js";
|
|
7
13
|
import { writeTaskProgress } from "../taskProgress.js";
|
|
8
14
|
import { PatchWardenError } from "../errors.js";
|
|
9
15
|
import { savePlan } from "./savePlan.js";
|
|
@@ -15,19 +21,34 @@ export function createTask(input) {
|
|
|
15
21
|
const config = getConfig();
|
|
16
22
|
const tasksDir = getTasksDir(config);
|
|
17
23
|
const plansDir = getPlansDir(config);
|
|
24
|
+
const executionMode = input.execution_mode || "execute";
|
|
25
|
+
// ── assessment_id execute mode: load record and override params ──
|
|
26
|
+
let assessmentRecord = null;
|
|
27
|
+
let effectiveInput = input;
|
|
28
|
+
if (executionMode === "execute" && input.assessment_id) {
|
|
29
|
+
assessmentRecord = readAssessment(input.assessment_id);
|
|
30
|
+
// Parameter mismatch check: if ChatGPT passes params that differ from the assessment, reject
|
|
31
|
+
effectiveInput = mergeAssessmentIntoInput(input, assessmentRecord);
|
|
32
|
+
}
|
|
33
|
+
// ── Validate required fields ──
|
|
34
|
+
// agent and repo_path are required unless assessment_id fills them.
|
|
35
|
+
// assess_only always requires them because the risk engine needs to check them.
|
|
36
|
+
if (!effectiveInput.agent || effectiveInput.agent.trim() === "") {
|
|
37
|
+
throw new PatchWardenError("agent_required", "create_task requires an agent name.", "Pass a configured agent name, or use assessment_id from a prior assess_only call.");
|
|
38
|
+
}
|
|
18
39
|
const planSources = [
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
40
|
+
effectiveInput.plan_id?.trim() ? "plan_id" : "",
|
|
41
|
+
effectiveInput.inline_plan?.trim() ? "inline_plan" : "",
|
|
42
|
+
effectiveInput.template ? "template" : "",
|
|
22
43
|
].filter(Boolean);
|
|
23
44
|
if (planSources.length !== 1) {
|
|
24
45
|
throw new PatchWardenError("invalid_plan_source", "create_task requires exactly one of plan_id, inline_plan, or template.", "Use an existing plan_id, pass inline_plan text, or choose one built-in template.");
|
|
25
46
|
}
|
|
26
|
-
if (
|
|
27
|
-
throw new PatchWardenError("invalid_task_template", `Unknown task template "${
|
|
47
|
+
if (effectiveInput.template && !TASK_TEMPLATE_NAMES.includes(effectiveInput.template)) {
|
|
48
|
+
throw new PatchWardenError("invalid_task_template", `Unknown task template "${effectiveInput.template}".`, `Use one of: ${TASK_TEMPLATE_NAMES.join(", ")}.`);
|
|
28
49
|
}
|
|
29
50
|
// Resolve repo alias if configured
|
|
30
|
-
let resolvedRepoPath =
|
|
51
|
+
let resolvedRepoPath = effectiveInput.repo_path?.trim() || "";
|
|
31
52
|
const aliases = config.repoAliases;
|
|
32
53
|
if (aliases && resolvedRepoPath && aliases[resolvedRepoPath]) {
|
|
33
54
|
resolvedRepoPath = aliases[resolvedRepoPath];
|
|
@@ -36,8 +57,8 @@ export function createTask(input) {
|
|
|
36
57
|
throw new PatchWardenError("repo_path_required", "create_task requires an explicit repo_path; PatchWarden will not default to workspaceRoot.", 'Pass a repository path inside workspaceRoot, for example repo_path: "my-project".', true, { operation: "create_task", safe_alternative: "Pass an existing repository directory under workspaceRoot." });
|
|
37
58
|
}
|
|
38
59
|
// Validate agent
|
|
39
|
-
if (!config.agents[
|
|
40
|
-
throw new PatchWardenError("agent_not_configured", `Unknown agent "${
|
|
60
|
+
if (!config.agents[effectiveInput.agent]) {
|
|
61
|
+
throw new PatchWardenError("agent_not_configured", `Unknown agent "${effectiveInput.agent}". Available: ${Object.keys(config.agents).join(", ")}`, "Call list_agents and use an available configured agent.");
|
|
41
62
|
}
|
|
42
63
|
// Validate repo_path is within workspace
|
|
43
64
|
const safeRepoPath = guardWorkspacePath(resolvedRepoPath, config.workspaceRoot);
|
|
@@ -49,93 +70,273 @@ export function createTask(input) {
|
|
|
49
70
|
}
|
|
50
71
|
// Runtime self-modification protection: refuse to modify the active
|
|
51
72
|
// PatchWarden runtime directory or its critical subdirectories.
|
|
52
|
-
|
|
53
|
-
const criticalDirs = ["dist", "src", "scripts", "release"];
|
|
54
|
-
if (safeRepoPath === runtimeRoot || safeRepoPath.startsWith(runtimeRoot + resolve("/")[0])) {
|
|
55
|
-
const isCritical = criticalDirs.some((dir) => safeRepoPath === join(runtimeRoot, dir) ||
|
|
56
|
-
safeRepoPath.startsWith(join(runtimeRoot, dir) + resolve("/")[0]));
|
|
57
|
-
if (safeRepoPath === runtimeRoot || isCritical) {
|
|
58
|
-
throw new PatchWardenError("runtime_self_modification_blocked", `repo_path "${resolvedRepoPath}" points to the active PatchWarden runtime or its critical subdirectories.`, "Use a dev copy or git worktree for PatchWarden development. The running MCP server must not be modified by a task.", true, {
|
|
59
|
-
operation: "create_task",
|
|
60
|
-
path: resolvedRepoPath,
|
|
61
|
-
resolved_repo_path: safeRepoPath,
|
|
62
|
-
runtime_root: runtimeRoot,
|
|
63
|
-
safe_alternative: "Clone or copy PatchWarden to a separate directory for development tasks.",
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
73
|
+
guardRuntimeSelfModification(safeRepoPath);
|
|
67
74
|
// Validate test command — must be in allowlist, no swallowing
|
|
68
75
|
let testCmd = "";
|
|
69
|
-
if (
|
|
70
|
-
testCmd = guardTestCommand(
|
|
76
|
+
if (effectiveInput.test_command && effectiveInput.test_command.trim() !== "") {
|
|
77
|
+
testCmd = guardTestCommand(effectiveInput.test_command, config, safeRepoPath);
|
|
71
78
|
// guardTestCommand throws if not in allowedTestCommands
|
|
72
79
|
}
|
|
73
|
-
if (
|
|
80
|
+
if (effectiveInput.verify_commands !== undefined && !Array.isArray(effectiveInput.verify_commands)) {
|
|
74
81
|
throw new PatchWardenError("invalid_verify_commands", "verify_commands must be an array of allow-listed command strings.", "Pass an array such as [\"npm test\", \"npm run build\"].");
|
|
75
82
|
}
|
|
76
|
-
if ((
|
|
83
|
+
if ((effectiveInput.verify_commands?.length || 0) > 20) {
|
|
77
84
|
throw new PatchWardenError("invalid_verify_commands", "verify_commands cannot contain more than 20 commands.", "Keep verification focused and use no more than 20 allow-listed commands.");
|
|
78
85
|
}
|
|
79
86
|
const verifyCommands = [...new Set([
|
|
80
|
-
...(
|
|
87
|
+
...(effectiveInput.verify_commands || []).map((command) => guardTestCommand(command, config, safeRepoPath)),
|
|
81
88
|
...(testCmd ? [testCmd] : []),
|
|
82
89
|
])];
|
|
83
|
-
const timeoutSeconds =
|
|
90
|
+
const timeoutSeconds = effectiveInput.timeout_seconds ?? config.defaultTaskTimeoutSeconds;
|
|
84
91
|
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds <= 0) {
|
|
85
92
|
throw new PatchWardenError("invalid_timeout", "timeout_seconds must be a positive integer", `Use a whole number from 1 to ${config.maxTaskTimeoutSeconds}.`);
|
|
86
93
|
}
|
|
87
94
|
if (timeoutSeconds > config.maxTaskTimeoutSeconds) {
|
|
88
95
|
throw new PatchWardenError("invalid_timeout", `timeout_seconds cannot exceed configured maximum ${config.maxTaskTimeoutSeconds}`, `Use a value no greater than ${config.maxTaskTimeoutSeconds}.`);
|
|
89
96
|
}
|
|
90
|
-
let planId =
|
|
97
|
+
let planId = effectiveInput.plan_id?.trim() || "";
|
|
91
98
|
let planSource = "saved";
|
|
92
99
|
let changePolicy = "repo_scoped_changes";
|
|
100
|
+
let planContentForHash = null;
|
|
101
|
+
// ── assess_only: check plan content WITHOUT saving first ──
|
|
102
|
+
// This catches guardPlanContent violations as hard rule hits instead of throwing.
|
|
103
|
+
if (executionMode === "assess_only") {
|
|
104
|
+
let assessPlanContent = "";
|
|
105
|
+
let assessPlanTitle = effectiveInput.plan_title?.trim() || "Assessment plan";
|
|
106
|
+
if (planId) {
|
|
107
|
+
const planFile = join(resolve(plansDir, planId), "plan.md");
|
|
108
|
+
guardReadPath(planFile, config.workspaceRoot, config.plansDir);
|
|
109
|
+
if (!existsSync(planFile)) {
|
|
110
|
+
throw new PatchWardenError("plan_not_found", `Plan "${planId}" not found.`, "Call save_plan first, or pass inline_plan/template directly to create_task.");
|
|
111
|
+
}
|
|
112
|
+
assessPlanContent = readFileSync(planFile, "utf-8");
|
|
113
|
+
}
|
|
114
|
+
else if (effectiveInput.inline_plan?.trim()) {
|
|
115
|
+
assessPlanContent = effectiveInput.inline_plan.trim();
|
|
116
|
+
}
|
|
117
|
+
else if (effectiveInput.template) {
|
|
118
|
+
const expanded = expandTaskTemplate({
|
|
119
|
+
template: effectiveInput.template,
|
|
120
|
+
goal: effectiveInput.goal || "",
|
|
121
|
+
source_task_id: effectiveInput.source_task_id,
|
|
122
|
+
verify_commands: verifyCommands,
|
|
123
|
+
});
|
|
124
|
+
assessPlanContent = expanded.content;
|
|
125
|
+
assessPlanTitle = expanded.title;
|
|
126
|
+
changePolicy = expanded.change_policy;
|
|
127
|
+
}
|
|
128
|
+
// Run risk engine WITHOUT saving the plan
|
|
129
|
+
let planBlocked = false;
|
|
130
|
+
let planBlockReason = "";
|
|
131
|
+
try {
|
|
132
|
+
guardPlanContent(assessPlanTitle, assessPlanContent);
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
planBlocked = true;
|
|
136
|
+
planBlockReason = e instanceof PatchWardenError ? e.reason : "plan_content_blocked";
|
|
137
|
+
}
|
|
138
|
+
const snapshot = captureRepoSnapshot(safeRepoPath);
|
|
139
|
+
const snapshotTruncated = snapshot.warnings.some((w) => w.includes("snapshot limited"));
|
|
140
|
+
let riskResult;
|
|
141
|
+
if (planBlocked) {
|
|
142
|
+
riskResult = {
|
|
143
|
+
risk_level: "high",
|
|
144
|
+
decision: "blocked",
|
|
145
|
+
reason_codes: [],
|
|
146
|
+
risk_hints: [],
|
|
147
|
+
hard_rule_hits: [planBlockReason],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
riskResult = assessRisk({
|
|
152
|
+
repoPath: resolvedRepoPath,
|
|
153
|
+
resolvedRepoPath: safeRepoPath,
|
|
154
|
+
planContent: assessPlanContent,
|
|
155
|
+
planTitle: assessPlanTitle,
|
|
156
|
+
testCommand: testCmd,
|
|
157
|
+
verifyCommands: verifyCommands,
|
|
158
|
+
template: effectiveInput.template,
|
|
159
|
+
goal: effectiveInput.goal,
|
|
160
|
+
agent: effectiveInput.agent,
|
|
161
|
+
config,
|
|
162
|
+
snapshotTruncated,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
// Save the plan now that risk assessment is done (for allow/needs_confirm)
|
|
166
|
+
if (!planBlocked) {
|
|
167
|
+
if (effectiveInput.inline_plan?.trim()) {
|
|
168
|
+
const saved = savePlan({
|
|
169
|
+
title: effectiveInput.plan_title?.trim() || "Inline task plan",
|
|
170
|
+
content: effectiveInput.inline_plan.trim(),
|
|
171
|
+
});
|
|
172
|
+
planId = saved.plan_id;
|
|
173
|
+
planSource = "inline";
|
|
174
|
+
planContentForHash = readFileSync(saved.path, "utf-8");
|
|
175
|
+
}
|
|
176
|
+
else if (effectiveInput.template) {
|
|
177
|
+
const saved = savePlan({ title: assessPlanTitle, content: assessPlanContent });
|
|
178
|
+
planId = saved.plan_id;
|
|
179
|
+
planSource = "template";
|
|
180
|
+
planContentForHash = readFileSync(saved.path, "utf-8");
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
planContentForHash = assessPlanContent;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// ── Agent assessment: only when enabled AND deterministic decision is "allow" ──
|
|
187
|
+
let finalRiskResult = riskResult;
|
|
188
|
+
let agentAssessmentSummary = null;
|
|
189
|
+
let preGeneratedAssessmentId;
|
|
190
|
+
let preGeneratedAssessmentDir;
|
|
191
|
+
if (config.enableAgentAssessment === true && riskResult.decision === "allow" && !planBlocked) {
|
|
192
|
+
preGeneratedAssessmentId = generateAssessmentId();
|
|
193
|
+
preGeneratedAssessmentDir = createAssessmentDir(preGeneratedAssessmentId);
|
|
194
|
+
const assessorAgentName = config.agentAssessmentAgentName || effectiveInput.agent;
|
|
195
|
+
agentAssessmentSummary = runAgentAssessment({
|
|
196
|
+
assessmentId: preGeneratedAssessmentId,
|
|
197
|
+
assessmentDir: preGeneratedAssessmentDir,
|
|
198
|
+
agentName: assessorAgentName,
|
|
199
|
+
repoPath: safeRepoPath,
|
|
200
|
+
workspaceRoot: config.workspaceRoot,
|
|
201
|
+
goal: effectiveInput.goal || "",
|
|
202
|
+
planContent: assessPlanContent,
|
|
203
|
+
timeoutSeconds: config.agentAssessmentTimeoutSeconds || 120,
|
|
204
|
+
maxOutputBytes: config.agentAssessmentMaxOutputBytes || 524288,
|
|
205
|
+
config,
|
|
206
|
+
});
|
|
207
|
+
finalRiskResult = {
|
|
208
|
+
risk_level: agentAssessmentSummary.merged_risk,
|
|
209
|
+
decision: agentAssessmentSummary.merged_decision,
|
|
210
|
+
reason_codes: [...riskResult.reason_codes, ...agentAssessmentSummary.merged_reason_codes],
|
|
211
|
+
risk_hints: riskResult.risk_hints,
|
|
212
|
+
hard_rule_hits: riskResult.hard_rule_hits,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const record = createAssessment({
|
|
216
|
+
decision: finalRiskResult.decision,
|
|
217
|
+
risk_level: finalRiskResult.risk_level,
|
|
218
|
+
risk_hints: finalRiskResult.risk_hints,
|
|
219
|
+
hard_rule_hits: finalRiskResult.hard_rule_hits,
|
|
220
|
+
reason_codes: finalRiskResult.reason_codes,
|
|
221
|
+
repo_path: resolvedRepoPath,
|
|
222
|
+
resolved_repo_path: safeRepoPath,
|
|
223
|
+
plan_id: planId || null,
|
|
224
|
+
plan_content: planContentForHash,
|
|
225
|
+
template: effectiveInput.template || null,
|
|
226
|
+
goal: effectiveInput.goal || null,
|
|
227
|
+
test_command: testCmd || null,
|
|
228
|
+
verify_commands: verifyCommands,
|
|
229
|
+
agent: effectiveInput.agent,
|
|
230
|
+
timeout_seconds: timeoutSeconds,
|
|
231
|
+
change_policy: changePolicy,
|
|
232
|
+
snapshot,
|
|
233
|
+
...(preGeneratedAssessmentId ? { assessment_id: preGeneratedAssessmentId } : {}),
|
|
234
|
+
...(preGeneratedAssessmentDir ? { assessment_dir: preGeneratedAssessmentDir } : {}),
|
|
235
|
+
agent_assessment_summary: agentAssessmentSummary,
|
|
236
|
+
});
|
|
237
|
+
const nextAction = record.decision === "allow"
|
|
238
|
+
? "Call the returned next_tool_call exactly as provided; do not resend goal, plan, repository, agent, or verification arguments."
|
|
239
|
+
: record.decision === "needs_confirm"
|
|
240
|
+
? `Assessment requires local confirmation. Run patchwarden-confirm ${record.assessment_id} locally, then call the returned next_tool_call exactly as provided.`
|
|
241
|
+
: `Assessment blocked. Fix the reported hard rule hit and re-run assess_only.`;
|
|
242
|
+
const nextToolCall = record.decision === "blocked" ? undefined : {
|
|
243
|
+
name: "create_task",
|
|
244
|
+
arguments: {
|
|
245
|
+
execution_mode: "execute",
|
|
246
|
+
assessment_id: record.assessment_id,
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
return {
|
|
250
|
+
assessment_id: record.assessment_id,
|
|
251
|
+
assessment_short_id: record.assessment_short_id,
|
|
252
|
+
decision: record.decision,
|
|
253
|
+
risk_level: record.risk_level,
|
|
254
|
+
risk_hints: record.risk_hints,
|
|
255
|
+
hard_rule_hits: record.hard_rule_hits,
|
|
256
|
+
reason_codes: record.reason_codes,
|
|
257
|
+
expires_at: record.expires_at,
|
|
258
|
+
requires_confirm: record.requires_confirm,
|
|
259
|
+
workspace_snapshot_summary: record.workspace_snapshot_summary,
|
|
260
|
+
next_action: nextAction,
|
|
261
|
+
...(nextToolCall ? { next_tool_call: nextToolCall } : {}),
|
|
262
|
+
local_confirmation: {
|
|
263
|
+
required: record.requires_confirm,
|
|
264
|
+
command: record.requires_confirm ? "patchwarden-confirm" : null,
|
|
265
|
+
arguments: record.requires_confirm ? [record.assessment_id] : [],
|
|
266
|
+
},
|
|
267
|
+
...(finalRiskResult.hard_rule_hits.length > 0
|
|
268
|
+
? { suggestion: `Hard rule hit: ${finalRiskResult.hard_rule_hits.join(", ")}` }
|
|
269
|
+
: {}),
|
|
270
|
+
...(agentAssessmentSummary ? {
|
|
271
|
+
agent_assessment: {
|
|
272
|
+
status: agentAssessmentSummary.status,
|
|
273
|
+
merged_risk: agentAssessmentSummary.merged_risk,
|
|
274
|
+
merged_decision: agentAssessmentSummary.merged_decision,
|
|
275
|
+
confidence: agentAssessmentSummary.output?.confidence ?? null,
|
|
276
|
+
notes: agentAssessmentSummary.output?.notes ?? null,
|
|
277
|
+
read_only_violation: agentAssessmentSummary.read_only_violation,
|
|
278
|
+
stdout_truncated: agentAssessmentSummary.stdout_truncated,
|
|
279
|
+
stderr_truncated: agentAssessmentSummary.stderr_truncated,
|
|
280
|
+
}
|
|
281
|
+
} : {}),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
// ── execute mode: resolve plan normally ──
|
|
93
285
|
if (planId) {
|
|
94
286
|
const planFile = join(resolve(plansDir, planId), "plan.md");
|
|
95
287
|
guardReadPath(planFile, config.workspaceRoot, config.plansDir);
|
|
96
288
|
if (!existsSync(planFile)) {
|
|
97
289
|
throw new PatchWardenError("plan_not_found", `Plan "${planId}" not found.`, "Call save_plan first, or pass inline_plan/template directly to create_task.");
|
|
98
290
|
}
|
|
291
|
+
planContentForHash = readFileSync(planFile, "utf-8");
|
|
99
292
|
}
|
|
100
|
-
else if (
|
|
293
|
+
else if (effectiveInput.inline_plan?.trim()) {
|
|
101
294
|
const saved = savePlan({
|
|
102
|
-
title:
|
|
103
|
-
content:
|
|
295
|
+
title: effectiveInput.plan_title?.trim() || "Inline task plan",
|
|
296
|
+
content: effectiveInput.inline_plan.trim(),
|
|
104
297
|
});
|
|
105
298
|
planId = saved.plan_id;
|
|
106
299
|
planSource = "inline";
|
|
300
|
+
planContentForHash = readFileSync(saved.path, "utf-8");
|
|
107
301
|
}
|
|
108
302
|
else {
|
|
109
303
|
const expanded = expandTaskTemplate({
|
|
110
|
-
template:
|
|
111
|
-
goal:
|
|
112
|
-
source_task_id:
|
|
304
|
+
template: effectiveInput.template,
|
|
305
|
+
goal: effectiveInput.goal || "",
|
|
306
|
+
source_task_id: effectiveInput.source_task_id,
|
|
113
307
|
verify_commands: verifyCommands,
|
|
114
308
|
});
|
|
115
309
|
const saved = savePlan({ title: expanded.title, content: expanded.content });
|
|
116
310
|
planId = saved.plan_id;
|
|
117
311
|
planSource = "template";
|
|
118
312
|
changePolicy = expanded.change_policy;
|
|
313
|
+
planContentForHash = readFileSync(saved.path, "utf-8");
|
|
119
314
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
315
|
+
// ── execute mode with assessment_id: validate freshness ──
|
|
316
|
+
if (assessmentRecord) {
|
|
317
|
+
const snapshot = captureRepoSnapshot(safeRepoPath);
|
|
318
|
+
const validation = validateAssessmentFreshness(input.assessment_id, snapshot);
|
|
319
|
+
if (!validation.valid) {
|
|
320
|
+
throw new PatchWardenError(validation.failure_reason || "assessment_validation_failed", `Assessment "${input.assessment_id}" is no longer valid: ${validation.failure_reason}`, "Re-run create_task with execution_mode=assess_only to get a fresh assessment_id.", true, { assessment_id: input.assessment_id, failure_reason: validation.failure_reason });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const { taskId, taskDir } = createTaskDirectory(tasksDir, config.workspaceRoot, config.tasksDir);
|
|
124
324
|
const status = "pending";
|
|
125
325
|
const statusFile = join(taskDir, "status.json");
|
|
126
326
|
const statusData = {
|
|
127
327
|
task_id: taskId,
|
|
128
328
|
plan_id: planId,
|
|
129
329
|
plan_source: planSource,
|
|
130
|
-
template:
|
|
330
|
+
template: effectiveInput.template || null,
|
|
131
331
|
change_policy: changePolicy,
|
|
132
|
-
agent:
|
|
332
|
+
agent: effectiveInput.agent,
|
|
133
333
|
workspace_root: resolve(config.workspaceRoot),
|
|
134
334
|
repo_path: resolvedRepoPath,
|
|
135
335
|
resolved_repo_path: safeRepoPath,
|
|
136
336
|
test_command: testCmd,
|
|
137
337
|
verify_commands: verifyCommands,
|
|
138
338
|
timeout_seconds: timeoutSeconds,
|
|
339
|
+
assessment_id: assessmentRecord?.assessment_id || input.assessment_id || null,
|
|
139
340
|
status,
|
|
140
341
|
phase: "queued",
|
|
141
342
|
created_at: new Date().toISOString(),
|
|
@@ -163,14 +364,14 @@ export function createTask(input) {
|
|
|
163
364
|
return {
|
|
164
365
|
task_id: taskId,
|
|
165
366
|
plan_id: planId,
|
|
166
|
-
agent:
|
|
367
|
+
agent: effectiveInput.agent,
|
|
167
368
|
status,
|
|
168
369
|
timeout_seconds: timeoutSeconds,
|
|
169
370
|
continuation_required: watcher.available && hasWaitForTask,
|
|
170
371
|
next_action: !watcher.available ? nextActionBlocked : hasWaitForTask ? nextActionWait : nextActionPoll,
|
|
171
372
|
path: taskDir,
|
|
172
373
|
plan_source: planSource,
|
|
173
|
-
...(
|
|
374
|
+
...(effectiveInput.template ? { template: effectiveInput.template } : {}),
|
|
174
375
|
change_policy: changePolicy,
|
|
175
376
|
server_version: PATCHWARDEN_VERSION,
|
|
176
377
|
tool_profile: catalog?.tool_profile || resolveToolProfile(config.toolProfile),
|
|
@@ -186,3 +387,53 @@ export function createTask(input) {
|
|
|
186
387
|
: { name: "get_task_status", arguments: { task_id: taskId } },
|
|
187
388
|
};
|
|
188
389
|
}
|
|
390
|
+
function mergeAssessmentIntoInput(input, record) {
|
|
391
|
+
// Use plan_id from the assessment record (the plan was already saved during assess_only).
|
|
392
|
+
// Do NOT set template/inline_plan alongside plan_id — createTask requires exactly one plan source.
|
|
393
|
+
const merged = {
|
|
394
|
+
...input,
|
|
395
|
+
plan_id: record.plan_id || input.plan_id,
|
|
396
|
+
template: undefined,
|
|
397
|
+
inline_plan: undefined,
|
|
398
|
+
goal: record.goal || input.goal,
|
|
399
|
+
agent: record.agent,
|
|
400
|
+
repo_path: record.repo_path,
|
|
401
|
+
test_command: record.test_command || undefined,
|
|
402
|
+
verify_commands: record.verify_commands || input.verify_commands,
|
|
403
|
+
timeout_seconds: record.timeout_seconds || input.timeout_seconds,
|
|
404
|
+
};
|
|
405
|
+
// Parameter mismatch check: if caller passed explicit params that differ from assessment
|
|
406
|
+
if (input.template && record.template && input.template !== record.template) {
|
|
407
|
+
throw new PatchWardenError("assessment_parameter_mismatch", `template mismatch: caller passed "${input.template}" but assessment has "${record.template}".`, "Do not override assessment-locked parameters. Use the same assessment_id as-is.", true, { field: "template", assessment_value: record.template, caller_value: input.template });
|
|
408
|
+
}
|
|
409
|
+
if (input.goal && record.goal && input.goal !== record.goal) {
|
|
410
|
+
throw new PatchWardenError("assessment_parameter_mismatch", `goal mismatch: caller passed a different goal than the assessment.`, "Do not override assessment-locked parameters. Use the same assessment_id as-is.", true, { field: "goal" });
|
|
411
|
+
}
|
|
412
|
+
if (input.repo_path && record.repo_path && input.repo_path !== record.repo_path) {
|
|
413
|
+
throw new PatchWardenError("assessment_parameter_mismatch", `repo_path mismatch: caller passed "${input.repo_path}" but assessment has "${record.repo_path}".`, "Do not override assessment-locked parameters. Use the same assessment_id as-is.", true, { field: "repo_path", assessment_value: record.repo_path, caller_value: input.repo_path });
|
|
414
|
+
}
|
|
415
|
+
return merged;
|
|
416
|
+
}
|
|
417
|
+
function createTaskDirectory(tasksDir, workspaceRoot, configuredTasksDir) {
|
|
418
|
+
guardPath(tasksDir, workspaceRoot, configuredTasksDir);
|
|
419
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
420
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
421
|
+
const timestamp = new Date().toISOString()
|
|
422
|
+
.replace(/[-:]/g, "")
|
|
423
|
+
.replace("T", "_")
|
|
424
|
+
.slice(0, 15);
|
|
425
|
+
const taskId = `task_${timestamp}_${randomBytes(3).toString("hex")}`;
|
|
426
|
+
const taskDir = resolve(tasksDir, taskId);
|
|
427
|
+
guardPath(taskDir, workspaceRoot, configuredTasksDir);
|
|
428
|
+
try {
|
|
429
|
+
mkdirSync(taskDir, { recursive: false });
|
|
430
|
+
return { taskId, taskDir };
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
if (error.code === "EEXIST")
|
|
434
|
+
continue;
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
throw new PatchWardenError("task_id_generation_failed", "Could not allocate a unique short task ID.", "Retry create_task; no task directory was created.");
|
|
439
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ClassifiedChange } from "../runner/changeCapture.js";
|
|
2
|
+
export interface FinalizeDirectSessionInput {
|
|
3
|
+
session_id: string;
|
|
4
|
+
}
|
|
5
|
+
export interface FinalizeDirectSessionOutput {
|
|
6
|
+
session_id: string;
|
|
7
|
+
changed_files_total: number;
|
|
8
|
+
source_changes: ClassifiedChange[];
|
|
9
|
+
tracked_build_artifacts: ClassifiedChange[];
|
|
10
|
+
runtime_generated_files: ClassifiedChange[];
|
|
11
|
+
suspicious_changes: ClassifiedChange[];
|
|
12
|
+
out_of_scope_changes: ClassifiedChange[];
|
|
13
|
+
verification_summary: Record<string, "passed" | "failed">;
|
|
14
|
+
diff_path: string;
|
|
15
|
+
summary_path: string;
|
|
16
|
+
finalized: boolean;
|
|
17
|
+
next_action: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function finalizeDirectSession(input: FinalizeDirectSessionInput): FinalizeDirectSessionOutput;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { join, resolve, relative, isAbsolute } from "node:path";
|
|
2
|
+
import { readDirectSession, finalizeDirectSessionRecord, getDirectSessionDir, } from "../direct/directSessionStore.js";
|
|
3
|
+
import { guardDirectSessionActive } from "../direct/directGuards.js";
|
|
4
|
+
import { captureRepoSnapshot, buildChangeArtifacts, } from "../runner/changeCapture.js";
|
|
5
|
+
// ── Main function ──────────────────────────────────────────────────
|
|
6
|
+
export function finalizeDirectSession(input) {
|
|
7
|
+
const { session_id } = input;
|
|
8
|
+
// 1. Read session and guard active (not expired, not finalized)
|
|
9
|
+
const session = readDirectSession(session_id);
|
|
10
|
+
guardDirectSessionActive(session);
|
|
11
|
+
// 2. Capture after snapshot
|
|
12
|
+
const afterSnapshot = captureRepoSnapshot(session.resolved_repo_path);
|
|
13
|
+
// 3. Build change artifacts from before/after snapshots
|
|
14
|
+
const changeArtifacts = buildChangeArtifacts(session.resolved_repo_path, session.workspace_snapshot_before, afterSnapshot);
|
|
15
|
+
// 4. Finalize session record — writes changed-files.json, diff.patch,
|
|
16
|
+
// summary.json, summary.md and marks the session as finalized.
|
|
17
|
+
finalizeDirectSessionRecord(session_id, afterSnapshot, changeArtifacts);
|
|
18
|
+
// 5. Build verification summary from session.verification_runs
|
|
19
|
+
const verification_summary = {};
|
|
20
|
+
for (const run of session.verification_runs) {
|
|
21
|
+
verification_summary[run.command] = run.passed ? "passed" : "failed";
|
|
22
|
+
}
|
|
23
|
+
// 6. Identify out-of-scope changes (paths outside resolved_repo_path)
|
|
24
|
+
const out_of_scope_changes = findOutOfScopeChanges(changeArtifacts, session.resolved_repo_path);
|
|
25
|
+
// 7. Build compact summary with categorized changes
|
|
26
|
+
const sessionDir = getDirectSessionDir(session_id);
|
|
27
|
+
return {
|
|
28
|
+
session_id,
|
|
29
|
+
changed_files_total: changeArtifacts.changed_files.length,
|
|
30
|
+
source_changes: changeArtifacts.artifact_hygiene.source_changes,
|
|
31
|
+
tracked_build_artifacts: changeArtifacts.artifact_hygiene.tracked_build_artifacts,
|
|
32
|
+
runtime_generated_files: changeArtifacts.artifact_hygiene.runtime_generated_files,
|
|
33
|
+
suspicious_changes: changeArtifacts.artifact_hygiene.suspicious_changes,
|
|
34
|
+
out_of_scope_changes,
|
|
35
|
+
verification_summary,
|
|
36
|
+
diff_path: join(sessionDir, "diff.patch"),
|
|
37
|
+
summary_path: join(sessionDir, "summary.md"),
|
|
38
|
+
finalized: true,
|
|
39
|
+
next_action: "Call audit_session to independently review the changes.",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
43
|
+
/**
|
|
44
|
+
* Filter changeArtifacts.changed_files for paths that resolve outside the
|
|
45
|
+
* session's resolved_repo_path and map them to ClassifiedChange format.
|
|
46
|
+
*/
|
|
47
|
+
function findOutOfScopeChanges(artifacts, resolvedRepoPath) {
|
|
48
|
+
const normalizedRepo = resolve(resolvedRepoPath);
|
|
49
|
+
return artifacts.changed_files
|
|
50
|
+
.filter((file) => {
|
|
51
|
+
const fullPath = resolve(normalizedRepo, file.path);
|
|
52
|
+
const rel = relative(normalizedRepo, fullPath);
|
|
53
|
+
return isAbsolute(rel) || rel.startsWith("..");
|
|
54
|
+
})
|
|
55
|
+
.map((file) => toClassifiedChange(file));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Map a ChangedFile to the ClassifiedChange format by adding a reason string.
|
|
59
|
+
*/
|
|
60
|
+
function toClassifiedChange(file) {
|
|
61
|
+
return {
|
|
62
|
+
path: file.path,
|
|
63
|
+
change: file.change,
|
|
64
|
+
tracked: file.tracked,
|
|
65
|
+
ignored: file.ignored,
|
|
66
|
+
kind: file.kind,
|
|
67
|
+
reason: classificationReason(file),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Produce a human-readable reason for a changed file classification.
|
|
72
|
+
* Mirrors the logic in changeCapture.ts classifyArtifactHygiene.
|
|
73
|
+
*/
|
|
74
|
+
function classificationReason(change) {
|
|
75
|
+
if (change.ignored)
|
|
76
|
+
return "untracked path is ignored by repository Git rules";
|
|
77
|
+
if (change.kind === "build_artifact" && change.tracked)
|
|
78
|
+
return "artifact-like path is tracked by Git and requires review";
|
|
79
|
+
if (change.kind === "build_artifact")
|
|
80
|
+
return "artifact-like path is not ignored and requires review";
|
|
81
|
+
if (change.kind === "runtime_generated")
|
|
82
|
+
return "runtime-generated path is not ignored and requires review";
|
|
83
|
+
return change.tracked ? "tracked source change" : "untracked source change";
|
|
84
|
+
}
|
|
@@ -62,5 +62,55 @@ export interface TaskSummaryOutput {
|
|
|
62
62
|
watcher: unknown;
|
|
63
63
|
pending_reason: string | null;
|
|
64
64
|
execution_blocked: boolean;
|
|
65
|
+
artifact_hygiene: Record<string, unknown>;
|
|
65
66
|
}
|
|
67
|
+
export interface CompactTaskSummaryOutput {
|
|
68
|
+
view: "compact";
|
|
69
|
+
task_id: string;
|
|
70
|
+
status: string;
|
|
71
|
+
terminal: boolean;
|
|
72
|
+
acceptance_status: TaskSummaryOutput["acceptance_status"];
|
|
73
|
+
phase: string;
|
|
74
|
+
repo_path: string;
|
|
75
|
+
changed_files_total: number;
|
|
76
|
+
out_of_scope_changes_total: number;
|
|
77
|
+
artifact_hygiene: {
|
|
78
|
+
counts: Record<string, number>;
|
|
79
|
+
source_changes: unknown[];
|
|
80
|
+
tracked_build_artifacts: unknown[];
|
|
81
|
+
ignored_untracked_artifacts: unknown[];
|
|
82
|
+
runtime_generated_files: unknown[];
|
|
83
|
+
suspicious_changes: unknown[];
|
|
84
|
+
max_items: number;
|
|
85
|
+
truncated: boolean;
|
|
86
|
+
};
|
|
87
|
+
release_artifacts_count: number;
|
|
88
|
+
release_artifact_paths: string[];
|
|
89
|
+
artifact_status: string | null;
|
|
90
|
+
verification_summary: TaskSummaryOutput["verification_summary"];
|
|
91
|
+
summary: string;
|
|
92
|
+
warnings: string[];
|
|
93
|
+
errors: string[];
|
|
94
|
+
failure_reason: string | null;
|
|
95
|
+
failed_command: string | null;
|
|
96
|
+
suggested_next_action: string;
|
|
97
|
+
execution_blocked: boolean;
|
|
98
|
+
pending_reason: string | null;
|
|
99
|
+
redacted: boolean;
|
|
100
|
+
redaction_categories: string[];
|
|
101
|
+
}
|
|
102
|
+
export interface GetTaskSummaryOptions {
|
|
103
|
+
view?: "compact" | "standard";
|
|
104
|
+
max_items?: number;
|
|
105
|
+
}
|
|
106
|
+
export type TaskSummaryResult = TaskSummaryOutput | CompactTaskSummaryOutput;
|
|
66
107
|
export declare function getTaskSummary(taskId: string): TaskSummaryOutput;
|
|
108
|
+
export declare function getTaskSummary(taskId: string, options: {
|
|
109
|
+
view: "compact";
|
|
110
|
+
max_items?: number;
|
|
111
|
+
}): CompactTaskSummaryOutput;
|
|
112
|
+
export declare function getTaskSummary(taskId: string, options: {
|
|
113
|
+
view?: "standard";
|
|
114
|
+
max_items?: number;
|
|
115
|
+
}): TaskSummaryOutput;
|
|
116
|
+
export declare function getTaskSummary(taskId: string, options: GetTaskSummaryOptions): TaskSummaryResult;
|