patchwarden 1.1.0 → 1.5.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/README.en.md +83 -8
- package/README.md +78 -10
- package/dist/assessments/agentAssessor.d.ts +1 -1
- package/dist/assessments/agentAssessor.js +3 -3
- package/dist/assessments/assessmentStore.d.ts +1 -1
- package/dist/assessments/assessmentStore.js +2 -2
- package/dist/assessments/confirmCli.js +5 -4
- package/dist/config.d.ts +7 -0
- package/dist/config.js +34 -0
- package/dist/control/middleware/auth.d.ts +10 -0
- package/dist/control/middleware/auth.js +8 -0
- package/dist/control/middleware/static.d.ts +3 -0
- package/dist/control/middleware/static.js +65 -0
- package/dist/control/routes/audit.d.ts +15 -0
- package/dist/control/routes/audit.js +277 -0
- package/dist/control/routes/evidence.d.ts +4 -0
- package/dist/control/routes/evidence.js +96 -0
- package/dist/control/routes/lineage.d.ts +3 -0
- package/dist/control/routes/lineage.js +71 -0
- package/dist/control/routes/policy.d.ts +3 -0
- package/dist/control/routes/policy.js +81 -0
- package/dist/control/routes/process.d.ts +5 -0
- package/dist/control/routes/process.js +200 -0
- package/dist/control/routes/sessions.d.ts +22 -0
- package/dist/control/routes/sessions.js +224 -0
- package/dist/control/routes/status.d.ts +6 -0
- package/dist/control/routes/status.js +250 -0
- package/dist/control/routes/taskActions.d.ts +21 -0
- package/dist/control/routes/taskActions.js +233 -0
- package/dist/control/routes/tasks.d.ts +20 -0
- package/dist/control/routes/tasks.js +310 -0
- package/dist/control/routes/workspace.d.ts +15 -0
- package/dist/control/routes/workspace.js +193 -0
- package/dist/control/runtime.d.ts +91 -0
- package/dist/control/runtime.js +392 -0
- package/dist/control/server.d.ts +13 -0
- package/dist/control/server.js +479 -0
- package/dist/control/shared.d.ts +35 -0
- package/dist/control/shared.js +288 -0
- package/dist/controlCenter.d.ts +6 -0
- package/dist/controlCenter.js +7 -2001
- package/dist/direct/directGuards.js +30 -8
- package/dist/direct/directSessionStore.d.ts +2 -0
- package/dist/direct/directVerification.js +7 -0
- package/dist/doctor.d.ts +18 -1
- package/dist/doctor.js +579 -348
- package/dist/goal/goalReport.d.ts +54 -0
- package/dist/goal/goalReport.js +204 -0
- package/dist/goal/goalStatus.d.ts +6 -0
- package/dist/goal/specKitImport.d.ts +63 -0
- package/dist/goal/specKitImport.js +220 -0
- package/dist/goal/subgoalSync.js +2 -1
- package/dist/httpServer.js +15 -12
- package/dist/index.js +7 -4
- package/dist/logging.d.ts +7 -1
- package/dist/logging.js +8 -0
- package/dist/policy/projectPolicy.d.ts +55 -0
- package/dist/policy/projectPolicy.js +286 -0
- package/dist/runner/changeCapture.d.ts +3 -3
- package/dist/runner/changeCapture.js +63 -39
- package/dist/runner/cli.js +7 -6
- package/dist/runner/postTaskCleanup.js +26 -2
- package/dist/runner/runTask.js +245 -221
- package/dist/runner/simpleProcess.js +4 -4
- package/dist/runner/watch.js +17 -14
- package/dist/security/contentRedaction.d.ts +6 -0
- package/dist/security/contentRedaction.js +22 -0
- package/dist/smoke-test.js +261 -255
- package/dist/test/unit/apply-patch.test.d.ts +1 -0
- package/dist/test/unit/apply-patch.test.js +225 -0
- package/dist/test/unit/create-task.test.d.ts +1 -0
- package/dist/test/unit/create-task.test.js +197 -0
- package/dist/test/unit/direct-guards.test.js +124 -9
- package/dist/test/unit/evidence-pack.test.d.ts +1 -0
- package/dist/test/unit/evidence-pack.test.js +224 -0
- package/dist/test/unit/get-task-status.test.d.ts +1 -0
- package/dist/test/unit/get-task-status.test.js +174 -0
- package/dist/test/unit/get-task-summary.test.d.ts +1 -0
- package/dist/test/unit/get-task-summary.test.js +146 -0
- package/dist/test/unit/goal-report.test.d.ts +1 -0
- package/dist/test/unit/goal-report.test.js +159 -0
- package/dist/test/unit/goal-subgoal-task.test.js +6 -6
- package/dist/test/unit/goal-tools-registry.test.js +6 -4
- package/dist/test/unit/path-guard.test.js +24 -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/run-task-loop.test.d.ts +1 -0
- package/dist/test/unit/run-task-loop.test.js +380 -0
- package/dist/test/unit/schema-drift-check.test.js +10 -9
- package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
- package/dist/test/unit/spec-kit-import.test.js +341 -0
- package/dist/test/unit/wait-for-task.test.d.ts +1 -0
- package/dist/test/unit/wait-for-task.test.js +144 -0
- package/dist/tools/auditTask.d.ts +8 -63
- package/dist/tools/auditTask.js +12 -8
- package/dist/tools/createDirectSession.d.ts +1 -1
- package/dist/tools/createDirectSession.js +2 -2
- package/dist/tools/createTask.d.ts +2 -2
- package/dist/tools/createTask.js +4 -4
- package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
- package/dist/tools/dispatch/coreDispatch.js +282 -0
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
- package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
- package/dist/tools/dispatch/directDispatch.d.ts +8 -0
- package/dist/tools/dispatch/directDispatch.js +115 -0
- package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
- package/dist/tools/dispatch/goalDispatch.js +91 -0
- package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
- package/dist/tools/dispatch/releaseDispatch.js +45 -0
- package/dist/tools/dispatch/types.d.ts +23 -0
- package/dist/tools/dispatch/types.js +15 -0
- package/dist/tools/evidencePack.d.ts +45 -0
- package/dist/tools/evidencePack.js +375 -0
- package/dist/tools/finalizeDirectSession.d.ts +1 -1
- package/dist/tools/finalizeDirectSession.js +3 -3
- package/dist/tools/goalSubgoalTask.d.ts +1 -1
- package/dist/tools/goalSubgoalTask.js +2 -2
- package/dist/tools/healthCheck.js +3 -3
- package/dist/tools/recommendAgentForTask.d.ts +19 -0
- package/dist/tools/recommendAgentForTask.js +56 -0
- package/dist/tools/registry.d.ts +3 -3
- package/dist/tools/registry.js +333 -402
- package/dist/tools/releaseMode.d.ts +50 -0
- package/dist/tools/releaseMode.js +370 -0
- package/dist/tools/retryTask.d.ts +2 -2
- package/dist/tools/retryTask.js +2 -2
- 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/safeViews.d.ts +8 -2
- package/dist/tools/safeViews.js +4 -2
- package/dist/tools/taskLineage.d.ts +91 -0
- package/dist/tools/taskLineage.js +175 -0
- package/dist/tools/toolCatalog.d.ts +2 -2
- package/dist/tools/toolCatalog.js +6 -0
- package/dist/tools/toolRegistry.js +132 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/docs/CODE_WIKI.md +983 -0
- package/docs/agentseal-integration.md +150 -0
- package/docs/architecture.md +63 -0
- package/docs/assets/patchwarden-oss-demo.gif +0 -0
- package/docs/chatgpt-usage.md +31 -0
- package/docs/control-center/README.md +9 -0
- package/docs/dashboard-overview.md +86 -0
- package/docs/demo.md +8 -0
- package/docs/direct-session-workflow.md +98 -0
- package/docs/evidence-pack-schema.md +215 -0
- package/docs/execution-plan-2026-07-09.md +315 -0
- package/docs/lineage-evidence-pack-workflow.md +127 -0
- package/docs/mcp-inspector-testing.md +200 -0
- package/docs/open-source-application.md +162 -0
- package/docs/opencode-worker.md +151 -0
- package/docs/openhands-worker.md +181 -0
- package/docs/release-evidence.md +72 -0
- package/docs/roadmap-execution-and-acceptance.md +365 -0
- package/docs/spec-kit-integration.md +131 -0
- package/docs/task-safe-review-workflow.md +98 -0
- package/docs/threat-model.md +79 -0
- package/docs/user-feedback.md +40 -0
- package/docs/why-patchwarden.md +110 -0
- package/package.json +3 -3
- package/scripts/checks/control-center-smoke.js +443 -0
- package/scripts/checks/control-smoke.js +2 -2
- package/scripts/checks/lifecycle-smoke.js +29 -23
- package/scripts/checks/mcp-manifest-check.js +12 -0
- package/scripts/checks/mcp-smoke.js +33 -7
- package/scripts/checks/watcher-supervisor-smoke.js +1 -1
- package/scripts/generate-demo-gif.py +320 -0
- package/src/assessments/agentAssessor.ts +3 -3
- package/src/assessments/assessmentStore.ts +2 -2
- package/src/assessments/confirmCli.ts +5 -4
- package/src/config.ts +37 -0
- package/src/control/middleware/auth.ts +17 -0
- package/src/control/middleware/static.ts +71 -0
- package/src/control/routes/audit.ts +321 -0
- package/src/control/routes/evidence.ts +107 -0
- package/src/control/routes/lineage.ts +92 -0
- package/src/control/routes/policy.ts +81 -0
- package/src/control/routes/process.ts +204 -0
- package/src/control/routes/sessions.ts +251 -0
- package/src/control/routes/status.ts +325 -0
- package/src/control/routes/taskActions.ts +248 -0
- package/src/control/routes/tasks.ts +323 -0
- package/src/control/routes/workspace.ts +203 -0
- package/src/control/runtime.ts +472 -0
- package/src/control/server.ts +471 -0
- package/src/control/shared.ts +294 -0
- package/src/controlCenter.ts +7 -2150
- package/src/direct/directGuards.ts +28 -7
- package/src/direct/directSessionStore.ts +2 -0
- package/src/direct/directVerification.ts +7 -0
- package/src/doctor.ts +741 -481
- package/src/goal/goalReport.ts +271 -0
- package/src/goal/goalStatus.ts +6 -0
- package/src/goal/specKitImport.ts +355 -0
- package/src/goal/subgoalSync.ts +4 -2
- package/src/httpServer.ts +17 -14
- package/src/index.ts +7 -4
- package/src/logging.ts +10 -1
- package/src/policy/projectPolicy.ts +344 -0
- package/src/runner/changeCapture.ts +70 -42
- package/src/runner/cli.ts +7 -6
- package/src/runner/postTaskCleanup.ts +26 -2
- package/src/runner/runTask.ts +325 -223
- package/src/runner/simpleProcess.ts +4 -4
- package/src/runner/watch.ts +17 -14
- package/src/security/contentRedaction.ts +29 -0
- package/src/smoke-test.ts +254 -252
- package/src/test/unit/apply-patch.test.ts +293 -0
- package/src/test/unit/create-task.test.ts +255 -0
- package/src/test/unit/direct-guards.test.ts +178 -8
- package/src/test/unit/evidence-pack.test.ts +251 -0
- package/src/test/unit/get-task-status.test.ts +203 -0
- package/src/test/unit/get-task-summary.test.ts +173 -0
- package/src/test/unit/goal-report.test.ts +189 -0
- package/src/test/unit/goal-subgoal-task.test.ts +6 -6
- package/src/test/unit/goal-tools-registry.test.ts +7 -5
- package/src/test/unit/path-guard.test.ts +30 -0
- package/src/test/unit/project-policy-release-mode.test.ts +156 -0
- package/src/test/unit/run-task-loop.test.ts +425 -0
- package/src/test/unit/schema-drift-check.test.ts +11 -9
- package/src/test/unit/spec-kit-import.test.ts +429 -0
- package/src/test/unit/wait-for-task.test.ts +176 -0
- package/src/tools/auditTask.ts +99 -59
- package/src/tools/createDirectSession.ts +3 -3
- package/src/tools/createTask.ts +7 -7
- package/src/tools/dispatch/coreDispatch.ts +374 -0
- package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
- package/src/tools/dispatch/directDispatch.ts +167 -0
- package/src/tools/dispatch/goalDispatch.ts +127 -0
- package/src/tools/dispatch/releaseDispatch.ts +65 -0
- package/src/tools/dispatch/types.ts +24 -0
- package/src/tools/evidencePack.ts +490 -0
- package/src/tools/finalizeDirectSession.ts +4 -4
- package/src/tools/goalSubgoalTask.ts +2 -2
- package/src/tools/healthCheck.ts +3 -3
- package/src/tools/listWorkspace.ts +71 -71
- package/src/tools/recommendAgentForTask.ts +79 -0
- package/src/tools/registry.ts +352 -509
- package/src/tools/releaseMode.ts +450 -0
- package/src/tools/retryTask.ts +2 -2
- package/src/tools/runDirectVerificationBundle.ts +98 -0
- package/src/tools/runTaskLoop.ts +526 -0
- package/src/tools/runVerification.ts +8 -0
- package/src/tools/safeViews.ts +4 -2
- package/src/tools/taskLineage.ts +300 -0
- package/src/tools/toolCatalog.ts +6 -0
- package/src/tools/toolRegistry.ts +132 -0
- package/src/version.ts +2 -2
- package/ui/pages/audit.html +192 -3
- package/ui/pages/dashboard.html +1070 -14
- package/ui/pages/direct-sessions.html +505 -53
- package/ui/pages/task-detail.html +456 -438
- package/ui/pages/tasks.html +598 -61
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { reloadConfig } from "../../config.js";
|
|
7
|
+
import { recommendAgentForTask } from "../../tools/recommendAgentForTask.js";
|
|
8
|
+
import { exportTaskEvidencePack, listEvidencePacks, readEvidencePack } from "../../tools/evidencePack.js";
|
|
9
|
+
import { writeTaskLineage } from "../../tools/taskLineage.js";
|
|
10
|
+
let tempDir;
|
|
11
|
+
let prevConfigEnv;
|
|
12
|
+
function writeConfig() {
|
|
13
|
+
const configPath = join(tempDir, "patchwarden.config.json");
|
|
14
|
+
writeFileSync(configPath, JSON.stringify({
|
|
15
|
+
workspaceRoot: tempDir,
|
|
16
|
+
tasksDir: ".patchwarden/tasks",
|
|
17
|
+
plansDir: ".patchwarden/plans",
|
|
18
|
+
assessmentsDir: ".patchwarden/assessments",
|
|
19
|
+
agents: {
|
|
20
|
+
codex: { command: "codex", args: [] },
|
|
21
|
+
opencode: { command: "opencode", args: [] },
|
|
22
|
+
},
|
|
23
|
+
allowedTestCommands: ["npm test", "npm run build"],
|
|
24
|
+
defaultTaskTimeoutSeconds: 30,
|
|
25
|
+
maxTaskTimeoutSeconds: 120,
|
|
26
|
+
}), "utf-8");
|
|
27
|
+
prevConfigEnv = process.env.PATCHWARDEN_CONFIG;
|
|
28
|
+
process.env.PATCHWARDEN_CONFIG = configPath;
|
|
29
|
+
reloadConfig();
|
|
30
|
+
}
|
|
31
|
+
describe("v1.5 evidence packs and agent recommendations", () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-evidence-"));
|
|
34
|
+
writeConfig();
|
|
35
|
+
});
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
if (prevConfigEnv === undefined)
|
|
38
|
+
delete process.env.PATCHWARDEN_CONFIG;
|
|
39
|
+
else
|
|
40
|
+
process.env.PATCHWARDEN_CONFIG = prevConfigEnv;
|
|
41
|
+
reloadConfig();
|
|
42
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
it("recommends an agent without creating task evidence", () => {
|
|
45
|
+
const recommendation = recommendAgentForTask({
|
|
46
|
+
repo_path: ".",
|
|
47
|
+
goal: "Refactor two modules safely",
|
|
48
|
+
scope_files: ["src/a.ts", "src/b.ts"],
|
|
49
|
+
});
|
|
50
|
+
assert.equal(recommendation.bounded, true);
|
|
51
|
+
assert.ok(["codex", "opencode"].includes(recommendation.recommended_agent));
|
|
52
|
+
assert.ok(Array.isArray(recommendation.suggested_verify_commands));
|
|
53
|
+
const payload = JSON.stringify(recommendation);
|
|
54
|
+
assert.ok(!payload.includes("stdout"));
|
|
55
|
+
assert.ok(!payload.includes("stderr"));
|
|
56
|
+
assert.ok(!payload.includes("diff"));
|
|
57
|
+
});
|
|
58
|
+
it("exports bounded BOM-free evidence pack from lineage", () => {
|
|
59
|
+
writeTaskLineage({
|
|
60
|
+
lineage_id: "lineage_v15_pack",
|
|
61
|
+
goal: "Export evidence",
|
|
62
|
+
repo_path: ".",
|
|
63
|
+
created_at: "2026-07-05T12:00:00.000Z",
|
|
64
|
+
updated_at: "2026-07-05T12:01:00.000Z",
|
|
65
|
+
final_status: "accepted",
|
|
66
|
+
stop_reason: "success",
|
|
67
|
+
next_action: "accept",
|
|
68
|
+
main_task: "task-main",
|
|
69
|
+
fix_tasks: [],
|
|
70
|
+
cleanup_tasks: [],
|
|
71
|
+
direct_sessions: [{
|
|
72
|
+
session_id: "direct-one",
|
|
73
|
+
status: "passed",
|
|
74
|
+
command_count: 1,
|
|
75
|
+
passed_commands: 1,
|
|
76
|
+
failed_commands: 0,
|
|
77
|
+
timed_out_commands: 0,
|
|
78
|
+
audit_decision: "pass",
|
|
79
|
+
changed_files_total: 0,
|
|
80
|
+
next_action: "accept",
|
|
81
|
+
}],
|
|
82
|
+
rounds: [{
|
|
83
|
+
iteration: 1,
|
|
84
|
+
task_id: "task-main",
|
|
85
|
+
role: "main",
|
|
86
|
+
status: "done_by_agent",
|
|
87
|
+
terminal: true,
|
|
88
|
+
verification_status: "passed",
|
|
89
|
+
audit_verdict: "pass",
|
|
90
|
+
fail_checks: [],
|
|
91
|
+
warn_checks: [],
|
|
92
|
+
next_action: "accept",
|
|
93
|
+
}],
|
|
94
|
+
warnings: [],
|
|
95
|
+
errors: [],
|
|
96
|
+
worktree: {
|
|
97
|
+
isolation_mode: "worktree",
|
|
98
|
+
worktree_id: "wt-one",
|
|
99
|
+
worktree_path: join(tempDir, "_workspacetrees", "wt-one"),
|
|
100
|
+
branch: "pw-test",
|
|
101
|
+
cleanup: "keep",
|
|
102
|
+
status: "active",
|
|
103
|
+
next_action: "merge or discard explicitly",
|
|
104
|
+
},
|
|
105
|
+
agent_routing: {
|
|
106
|
+
requested_agent: "auto",
|
|
107
|
+
selected_agent: "codex",
|
|
108
|
+
reason: "test route",
|
|
109
|
+
fallback: false,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
const pack = exportTaskEvidencePack({ lineage_id: "lineage_v15_pack" });
|
|
113
|
+
assert.equal(pack.bounded, true);
|
|
114
|
+
assert.equal(pack.lineage.worktree.worktree_id, "wt-one");
|
|
115
|
+
assert.equal(pack.lineage.agent_routing?.selected_agent, "codex");
|
|
116
|
+
const raw = readFileSync(pack.files.json);
|
|
117
|
+
assert.notEqual(raw[0], 0xef);
|
|
118
|
+
JSON.parse(raw.toString("utf-8"));
|
|
119
|
+
const payload = JSON.stringify(pack);
|
|
120
|
+
assert.ok(!payload.includes("stdout_tail"));
|
|
121
|
+
assert.ok(!payload.includes("stderr_tail"));
|
|
122
|
+
assert.ok(!payload.includes("diff.patch"));
|
|
123
|
+
assert.ok(!payload.includes("verification.log"));
|
|
124
|
+
const readBack = readEvidencePack("lineage_v15_pack");
|
|
125
|
+
assert.equal(readBack?.lineage_id, "lineage_v15_pack");
|
|
126
|
+
const listed = listEvidencePacks();
|
|
127
|
+
assert.equal(listed.total, 1);
|
|
128
|
+
assert.equal(listed.evidence_packs[0].lineage_id, "lineage_v15_pack");
|
|
129
|
+
// v2: all six structured artifact files should exist.
|
|
130
|
+
for (const key of ["risk", "verify", "diffstat", "lineage", "attestation", "redactions"]) {
|
|
131
|
+
assert.ok(existsSync(pack.files[key]), `${key} file should exist`);
|
|
132
|
+
}
|
|
133
|
+
// v2: attestation.json must carry the required provenance fields.
|
|
134
|
+
const attestation = JSON.parse(readFileSync(pack.files.attestation, "utf-8"));
|
|
135
|
+
assert.ok(attestation.patchwarden_version, "attestation should have patchwarden_version");
|
|
136
|
+
assert.ok(attestation.package_version, "attestation should have package_version");
|
|
137
|
+
assert.ok(attestation.commit, "attestation should have commit");
|
|
138
|
+
assert.ok(attestation.node_version, "attestation should have node_version");
|
|
139
|
+
assert.ok(attestation.os, "attestation should have os");
|
|
140
|
+
assert.ok(attestation.schema_epoch, "attestation should have schema_epoch");
|
|
141
|
+
// v2: redactions.json must have the audit structure even when empty.
|
|
142
|
+
const redactions = JSON.parse(readFileSync(pack.files.redactions, "utf-8"));
|
|
143
|
+
assert.ok(Array.isArray(redactions.redactions), "redactions should be an array");
|
|
144
|
+
assert.equal(typeof redactions.total_redacted, "number", "total_redacted should be a number");
|
|
145
|
+
});
|
|
146
|
+
it("exports v2 structured artifacts and redacts secrets in diffstat", () => {
|
|
147
|
+
const secretValue = "ghp_1234567890abcdefghijklmnop";
|
|
148
|
+
// Create a task directory with file-stats.json containing a secret in a file path.
|
|
149
|
+
const taskDir = join(tempDir, ".patchwarden", "tasks", "task-v2-secret");
|
|
150
|
+
mkdirSync(taskDir, { recursive: true });
|
|
151
|
+
writeFileSync(join(taskDir, "file-stats.json"), JSON.stringify({
|
|
152
|
+
task_id: "task-v2-secret",
|
|
153
|
+
additions: 5,
|
|
154
|
+
deletions: 2,
|
|
155
|
+
files: [{
|
|
156
|
+
path: `secrets/${secretValue}.json`,
|
|
157
|
+
status: "modified",
|
|
158
|
+
additions: 5,
|
|
159
|
+
deletions: 2,
|
|
160
|
+
}],
|
|
161
|
+
}), "utf-8");
|
|
162
|
+
writeTaskLineage({
|
|
163
|
+
lineage_id: "lineage_v2_secret",
|
|
164
|
+
goal: "Export v2 evidence with diffstat",
|
|
165
|
+
repo_path: ".",
|
|
166
|
+
created_at: "2026-07-09T12:00:00.000Z",
|
|
167
|
+
updated_at: "2026-07-09T12:01:00.000Z",
|
|
168
|
+
final_status: "accepted",
|
|
169
|
+
stop_reason: "success",
|
|
170
|
+
next_action: "accept",
|
|
171
|
+
main_task: "task-v2-secret",
|
|
172
|
+
fix_tasks: [],
|
|
173
|
+
cleanup_tasks: [],
|
|
174
|
+
direct_sessions: [],
|
|
175
|
+
rounds: [{
|
|
176
|
+
iteration: 1,
|
|
177
|
+
task_id: "task-v2-secret",
|
|
178
|
+
role: "main",
|
|
179
|
+
status: "done_by_agent",
|
|
180
|
+
terminal: true,
|
|
181
|
+
verification_status: "passed",
|
|
182
|
+
audit_verdict: "pass",
|
|
183
|
+
fail_checks: [],
|
|
184
|
+
warn_checks: ["minor scope drift"],
|
|
185
|
+
next_action: "accept",
|
|
186
|
+
}],
|
|
187
|
+
warnings: [],
|
|
188
|
+
errors: [],
|
|
189
|
+
worktree: {
|
|
190
|
+
isolation_mode: "current_repo",
|
|
191
|
+
cleanup: "keep",
|
|
192
|
+
status: "not_used",
|
|
193
|
+
next_action: "none",
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
const pack = exportTaskEvidencePack({ lineage_id: "lineage_v2_secret" });
|
|
197
|
+
// All six v2 files should exist.
|
|
198
|
+
for (const key of ["risk", "verify", "diffstat", "lineage", "attestation", "redactions"]) {
|
|
199
|
+
assert.ok(existsSync(pack.files[key]), `${key} file should exist`);
|
|
200
|
+
}
|
|
201
|
+
// The original secret must not appear in any v2 file.
|
|
202
|
+
for (const key of ["risk", "verify", "diffstat", "lineage", "attestation", "redactions"]) {
|
|
203
|
+
const content = readFileSync(pack.files[key], "utf-8");
|
|
204
|
+
assert.ok(!content.includes(secretValue), `${key} should not contain the original secret value`);
|
|
205
|
+
}
|
|
206
|
+
// redactions.json should have detected the secret via diffstat aggregation.
|
|
207
|
+
const redactions = JSON.parse(readFileSync(pack.files.redactions, "utf-8"));
|
|
208
|
+
assert.ok(redactions.total_redacted > 0, "redactions.json should have detected the secret");
|
|
209
|
+
assert.ok(!JSON.stringify(redactions).includes(secretValue), "redactions.json should not contain the original secret value");
|
|
210
|
+
// risk.json should carry the warn_check as a medium-severity risk.
|
|
211
|
+
const risk = JSON.parse(readFileSync(pack.files.risk, "utf-8"));
|
|
212
|
+
assert.ok(risk.count > 0, "risk.json should have at least one risk item");
|
|
213
|
+
assert.equal(risk.by_severity.medium, 1, "should have one medium-severity risk");
|
|
214
|
+
// verify.json should carry the round verification record.
|
|
215
|
+
const verify = JSON.parse(readFileSync(pack.files.verify, "utf-8"));
|
|
216
|
+
assert.ok(verify.count > 0, "verify.json should have at least one record");
|
|
217
|
+
assert.equal(verify.records[0].verification_status, "passed");
|
|
218
|
+
// lineage.json should carry the bounded summary.
|
|
219
|
+
const lineageSummary = JSON.parse(readFileSync(pack.files.lineage, "utf-8"));
|
|
220
|
+
assert.equal(lineageSummary.lineage_id, "lineage_v2_secret");
|
|
221
|
+
assert.equal(lineageSummary.iterations_count, 1);
|
|
222
|
+
assert.equal(lineageSummary.final_status, "accepted");
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { reloadConfig } from "../../config.js";
|
|
7
|
+
import { getTaskStatus } from "../../tools/getTaskStatus.js";
|
|
8
|
+
let tempDir;
|
|
9
|
+
let tasksDir;
|
|
10
|
+
let prevConfigEnv;
|
|
11
|
+
function writeConfig() {
|
|
12
|
+
const configPath = join(tempDir, "patchwarden.config.json");
|
|
13
|
+
writeFileSync(configPath, JSON.stringify({
|
|
14
|
+
workspaceRoot: tempDir,
|
|
15
|
+
tasksDir: ".patchwarden/tasks",
|
|
16
|
+
plansDir: ".patchwarden/plans",
|
|
17
|
+
assessmentsDir: ".patchwarden/assessments",
|
|
18
|
+
directSessionsDir: ".patchwarden/direct-sessions",
|
|
19
|
+
agents: { codex: { command: "codex", args: ["exec", "{prompt}"] } },
|
|
20
|
+
allowedTestCommands: ["npm test"],
|
|
21
|
+
}), "utf-8");
|
|
22
|
+
prevConfigEnv = process.env.PATCHWARDEN_CONFIG;
|
|
23
|
+
process.env.PATCHWARDEN_CONFIG = configPath;
|
|
24
|
+
reloadConfig();
|
|
25
|
+
}
|
|
26
|
+
function writeTaskStatus(taskId, status) {
|
|
27
|
+
const taskDir = join(tasksDir, taskId);
|
|
28
|
+
mkdirSync(taskDir, { recursive: true });
|
|
29
|
+
writeFileSync(join(taskDir, "status.json"), JSON.stringify(status, null, 2), "utf-8");
|
|
30
|
+
return taskDir;
|
|
31
|
+
}
|
|
32
|
+
function writeTaskRuntime(taskDir, runtime) {
|
|
33
|
+
writeFileSync(join(taskDir, "runtime.json"), JSON.stringify(runtime, null, 2), "utf-8");
|
|
34
|
+
}
|
|
35
|
+
describe("getTaskStatus", () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-getstatus-"));
|
|
38
|
+
tasksDir = join(tempDir, ".patchwarden", "tasks");
|
|
39
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
40
|
+
writeConfig();
|
|
41
|
+
});
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
if (prevConfigEnv === undefined)
|
|
44
|
+
delete process.env.PATCHWARDEN_CONFIG;
|
|
45
|
+
else
|
|
46
|
+
process.env.PATCHWARDEN_CONFIG = prevConfigEnv;
|
|
47
|
+
reloadConfig();
|
|
48
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
49
|
+
});
|
|
50
|
+
// ── pending ──
|
|
51
|
+
it("reads pending status task", () => {
|
|
52
|
+
writeTaskStatus("task-pending-001", {
|
|
53
|
+
task_id: "task-pending-001",
|
|
54
|
+
plan_id: "plan-001",
|
|
55
|
+
agent: "codex",
|
|
56
|
+
workspace_root: tempDir,
|
|
57
|
+
repo_path: "repo",
|
|
58
|
+
resolved_repo_path: join(tempDir, "repo"),
|
|
59
|
+
status: "pending",
|
|
60
|
+
phase: "queued",
|
|
61
|
+
created_at: "2026-07-12T10:00:00Z",
|
|
62
|
+
updated_at: "2026-07-12T10:00:01Z",
|
|
63
|
+
timeout_seconds: 900,
|
|
64
|
+
error: null,
|
|
65
|
+
});
|
|
66
|
+
const result = getTaskStatus("task-pending-001");
|
|
67
|
+
assert.equal(result.task_id, "task-pending-001");
|
|
68
|
+
assert.equal(result.status, "pending");
|
|
69
|
+
assert.equal(result.phase, "queued");
|
|
70
|
+
assert.equal(result.agent, "codex");
|
|
71
|
+
assert.equal(result.plan_id, "plan-001");
|
|
72
|
+
assert.equal(result.error, null);
|
|
73
|
+
// No watcher heartbeat → watcher missing → pending task is execution_blocked
|
|
74
|
+
assert.equal(result.execution_blocked, true);
|
|
75
|
+
assert.equal(result.watcher_status, "missing");
|
|
76
|
+
assert.equal(result.pending_reason, "queued_but_watcher_missing");
|
|
77
|
+
});
|
|
78
|
+
// ── running ──
|
|
79
|
+
it("reads running status task and merges runtime.json", () => {
|
|
80
|
+
const taskDir = writeTaskStatus("task-running-001", {
|
|
81
|
+
task_id: "task-running-001",
|
|
82
|
+
plan_id: "plan-001",
|
|
83
|
+
agent: "codex",
|
|
84
|
+
workspace_root: tempDir,
|
|
85
|
+
repo_path: "repo",
|
|
86
|
+
resolved_repo_path: join(tempDir, "repo"),
|
|
87
|
+
status: "running",
|
|
88
|
+
phase: "executing_agent",
|
|
89
|
+
created_at: "2026-07-12T10:00:00Z",
|
|
90
|
+
started_at: "2026-07-12T10:00:05Z",
|
|
91
|
+
updated_at: "2026-07-12T10:00:10Z",
|
|
92
|
+
timeout_seconds: 900,
|
|
93
|
+
error: null,
|
|
94
|
+
});
|
|
95
|
+
writeTaskRuntime(taskDir, {
|
|
96
|
+
phase: "executing_agent",
|
|
97
|
+
last_heartbeat_at: "2026-07-12T10:00:12Z",
|
|
98
|
+
current_command: "codex exec --prompt",
|
|
99
|
+
});
|
|
100
|
+
const result = getTaskStatus("task-running-001");
|
|
101
|
+
assert.equal(result.status, "running");
|
|
102
|
+
assert.equal(result.phase, "executing_agent");
|
|
103
|
+
assert.equal(result.current_command, "codex exec --prompt");
|
|
104
|
+
assert.equal(result.last_heartbeat_at, "2026-07-12T10:00:12Z");
|
|
105
|
+
assert.equal(result.started_at, "2026-07-12T10:00:05Z");
|
|
106
|
+
// running (not pending) → execution_blocked is false
|
|
107
|
+
assert.equal(result.execution_blocked, false);
|
|
108
|
+
assert.equal(result.pending_reason, "agent_running");
|
|
109
|
+
});
|
|
110
|
+
// ── done ──
|
|
111
|
+
it("reads done status task", () => {
|
|
112
|
+
writeTaskStatus("task-done-001", {
|
|
113
|
+
task_id: "task-done-001",
|
|
114
|
+
plan_id: "plan-001",
|
|
115
|
+
agent: "codex",
|
|
116
|
+
workspace_root: tempDir,
|
|
117
|
+
repo_path: "repo",
|
|
118
|
+
resolved_repo_path: join(tempDir, "repo"),
|
|
119
|
+
status: "done",
|
|
120
|
+
phase: "completed",
|
|
121
|
+
created_at: "2026-07-12T10:00:00Z",
|
|
122
|
+
started_at: "2026-07-12T10:00:05Z",
|
|
123
|
+
finished_at: "2026-07-12T10:05:00Z",
|
|
124
|
+
updated_at: "2026-07-12T10:05:00Z",
|
|
125
|
+
timeout_seconds: 900,
|
|
126
|
+
verify_status: "passed",
|
|
127
|
+
error: null,
|
|
128
|
+
});
|
|
129
|
+
const result = getTaskStatus("task-done-001");
|
|
130
|
+
assert.equal(result.status, "done");
|
|
131
|
+
assert.equal(result.phase, "completed");
|
|
132
|
+
assert.equal(result.finished_at, "2026-07-12T10:05:00Z");
|
|
133
|
+
assert.equal(result.verify_status, "passed");
|
|
134
|
+
assert.equal(result.execution_blocked, false);
|
|
135
|
+
assert.equal(result.pending_reason, null);
|
|
136
|
+
});
|
|
137
|
+
// ── failed ──
|
|
138
|
+
it("reads failed status task", () => {
|
|
139
|
+
writeTaskStatus("task-failed-001", {
|
|
140
|
+
task_id: "task-failed-001",
|
|
141
|
+
plan_id: "plan-001",
|
|
142
|
+
agent: "codex",
|
|
143
|
+
workspace_root: tempDir,
|
|
144
|
+
repo_path: "repo",
|
|
145
|
+
resolved_repo_path: join(tempDir, "repo"),
|
|
146
|
+
status: "failed",
|
|
147
|
+
phase: "failed",
|
|
148
|
+
created_at: "2026-07-12T10:00:00Z",
|
|
149
|
+
started_at: "2026-07-12T10:00:05Z",
|
|
150
|
+
finished_at: "2026-07-12T10:05:00Z",
|
|
151
|
+
updated_at: "2026-07-12T10:05:00Z",
|
|
152
|
+
timeout_seconds: 900,
|
|
153
|
+
error: "Agent exited with code 1",
|
|
154
|
+
});
|
|
155
|
+
const result = getTaskStatus("task-failed-001");
|
|
156
|
+
assert.equal(result.status, "failed");
|
|
157
|
+
assert.equal(result.phase, "failed");
|
|
158
|
+
assert.equal(result.error, "Agent exited with code 1");
|
|
159
|
+
assert.equal(result.finished_at, "2026-07-12T10:05:00Z");
|
|
160
|
+
assert.equal(result.execution_blocked, false);
|
|
161
|
+
});
|
|
162
|
+
// ── status.json missing ──
|
|
163
|
+
it("throws when status.json is missing (task not found)", () => {
|
|
164
|
+
// guardReadPath throws "File not found" before the existsSync "Task not found" check
|
|
165
|
+
assert.throws(() => getTaskStatus("non-existent-task"), /(File not found|Task not found)/);
|
|
166
|
+
});
|
|
167
|
+
// ── status.json corrupt ──
|
|
168
|
+
it("throws when status.json is corrupt", () => {
|
|
169
|
+
const taskDir = join(tasksDir, "task-corrupt-001");
|
|
170
|
+
mkdirSync(taskDir, { recursive: true });
|
|
171
|
+
writeFileSync(join(taskDir, "status.json"), "{not valid json", "utf-8");
|
|
172
|
+
assert.throws(() => getTaskStatus("task-corrupt-001"), SyntaxError);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { reloadConfig } from "../../config.js";
|
|
7
|
+
import { getTaskSummary } from "../../tools/getTaskSummary.js";
|
|
8
|
+
let tempDir;
|
|
9
|
+
let tasksDir;
|
|
10
|
+
let prevConfigEnv;
|
|
11
|
+
function writeConfig() {
|
|
12
|
+
const configPath = join(tempDir, "patchwarden.config.json");
|
|
13
|
+
writeFileSync(configPath, JSON.stringify({
|
|
14
|
+
workspaceRoot: tempDir,
|
|
15
|
+
tasksDir: ".patchwarden/tasks",
|
|
16
|
+
plansDir: ".patchwarden/plans",
|
|
17
|
+
assessmentsDir: ".patchwarden/assessments",
|
|
18
|
+
directSessionsDir: ".patchwarden/direct-sessions",
|
|
19
|
+
agents: { codex: { command: "codex", args: ["exec", "{prompt}"] } },
|
|
20
|
+
allowedTestCommands: ["npm test"],
|
|
21
|
+
}), "utf-8");
|
|
22
|
+
prevConfigEnv = process.env.PATCHWARDEN_CONFIG;
|
|
23
|
+
process.env.PATCHWARDEN_CONFIG = configPath;
|
|
24
|
+
reloadConfig();
|
|
25
|
+
}
|
|
26
|
+
function writeTaskStatus(taskId, status) {
|
|
27
|
+
const taskDir = join(tasksDir, taskId);
|
|
28
|
+
mkdirSync(taskDir, { recursive: true });
|
|
29
|
+
writeFileSync(join(taskDir, "status.json"), JSON.stringify(status, null, 2), "utf-8");
|
|
30
|
+
return taskDir;
|
|
31
|
+
}
|
|
32
|
+
function baseStatus(taskId, overrides = {}) {
|
|
33
|
+
return {
|
|
34
|
+
task_id: taskId,
|
|
35
|
+
plan_id: "plan-001",
|
|
36
|
+
agent: "codex",
|
|
37
|
+
workspace_root: tempDir,
|
|
38
|
+
repo_path: "repo",
|
|
39
|
+
resolved_repo_path: join(tempDir, "repo"),
|
|
40
|
+
created_at: "2026-07-12T10:00:00Z",
|
|
41
|
+
started_at: "2026-07-12T10:00:05Z",
|
|
42
|
+
finished_at: "2026-07-12T10:05:00Z",
|
|
43
|
+
updated_at: "2026-07-12T10:05:00Z",
|
|
44
|
+
timeout_seconds: 900,
|
|
45
|
+
error: null,
|
|
46
|
+
...overrides,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
describe("getTaskSummary", () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-getsummary-"));
|
|
52
|
+
tasksDir = join(tempDir, ".patchwarden", "tasks");
|
|
53
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
54
|
+
writeConfig();
|
|
55
|
+
});
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
if (prevConfigEnv === undefined)
|
|
58
|
+
delete process.env.PATCHWARDEN_CONFIG;
|
|
59
|
+
else
|
|
60
|
+
process.env.PATCHWARDEN_CONFIG = prevConfigEnv;
|
|
61
|
+
reloadConfig();
|
|
62
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
63
|
+
});
|
|
64
|
+
// ── compact mode ──
|
|
65
|
+
it("returns compact summary in compact mode", () => {
|
|
66
|
+
writeTaskStatus("task-compact-001", baseStatus("task-compact-001", {
|
|
67
|
+
status: "done",
|
|
68
|
+
phase: "completed",
|
|
69
|
+
verify_status: "passed",
|
|
70
|
+
}));
|
|
71
|
+
const result = getTaskSummary("task-compact-001", { view: "compact" });
|
|
72
|
+
assert.equal(result.view, "compact");
|
|
73
|
+
assert.equal(result.task_id, "task-compact-001");
|
|
74
|
+
assert.equal(result.status, "done");
|
|
75
|
+
assert.equal(result.terminal, true);
|
|
76
|
+
// compact-only fields
|
|
77
|
+
assert.equal(typeof result.changed_files_total, "number");
|
|
78
|
+
assert.equal(typeof result.release_artifacts_count, "number");
|
|
79
|
+
assert.ok(result.artifact_hygiene);
|
|
80
|
+
assert.ok("max_items" in result.artifact_hygiene);
|
|
81
|
+
});
|
|
82
|
+
// ── full / standard mode ──
|
|
83
|
+
it("returns full summary in standard mode", () => {
|
|
84
|
+
writeTaskStatus("task-full-001", baseStatus("task-full-001", {
|
|
85
|
+
status: "done",
|
|
86
|
+
phase: "completed",
|
|
87
|
+
verify_status: "passed",
|
|
88
|
+
}));
|
|
89
|
+
const result = getTaskSummary("task-full-001");
|
|
90
|
+
// standard mode has no `view` field
|
|
91
|
+
assert.equal(result.view, undefined);
|
|
92
|
+
assert.equal(result.task_id, "task-full-001");
|
|
93
|
+
assert.equal(result.status, "done");
|
|
94
|
+
assert.equal(result.terminal, true);
|
|
95
|
+
// full-mode-only fields
|
|
96
|
+
assert.ok("log_tails" in result);
|
|
97
|
+
assert.ok("artifacts" in result);
|
|
98
|
+
assert.ok("failed_command_detail" in result);
|
|
99
|
+
assert.ok("acceptance_reviewed_at" in result);
|
|
100
|
+
});
|
|
101
|
+
// ── task not found ──
|
|
102
|
+
it("throws when task does not exist", () => {
|
|
103
|
+
assert.throws(() => getTaskSummary("non-existent-task"), /(File not found|Task not found)/);
|
|
104
|
+
});
|
|
105
|
+
// ── terminal: done ──
|
|
106
|
+
it("returns terminal summary for done task with passed verification", () => {
|
|
107
|
+
writeTaskStatus("task-done-001", baseStatus("task-done-001", {
|
|
108
|
+
status: "done",
|
|
109
|
+
phase: "completed",
|
|
110
|
+
verify_status: "passed",
|
|
111
|
+
}));
|
|
112
|
+
const result = getTaskSummary("task-done-001");
|
|
113
|
+
assert.equal(result.terminal, true);
|
|
114
|
+
assert.equal(result.status, "done");
|
|
115
|
+
assert.equal(result.verify_status, "passed");
|
|
116
|
+
// done + verify passed → ready_for_review
|
|
117
|
+
assert.equal(result.acceptance_status, "ready_for_review");
|
|
118
|
+
});
|
|
119
|
+
// ── terminal: failed ──
|
|
120
|
+
it("returns terminal summary for failed task", () => {
|
|
121
|
+
writeTaskStatus("task-failed-001", baseStatus("task-failed-001", {
|
|
122
|
+
status: "failed",
|
|
123
|
+
phase: "failed",
|
|
124
|
+
error: "Agent exited with code 1",
|
|
125
|
+
}));
|
|
126
|
+
const result = getTaskSummary("task-failed-001");
|
|
127
|
+
assert.equal(result.terminal, true);
|
|
128
|
+
assert.equal(result.status, "failed");
|
|
129
|
+
assert.equal(result.acceptance_status, "failed");
|
|
130
|
+
assert.ok(result.errors.includes("Agent exited with code 1"));
|
|
131
|
+
});
|
|
132
|
+
// ── running (non-terminal) ──
|
|
133
|
+
it("returns non-terminal summary for running task", () => {
|
|
134
|
+
writeTaskStatus("task-running-001", baseStatus("task-running-001", {
|
|
135
|
+
status: "running",
|
|
136
|
+
phase: "executing_agent",
|
|
137
|
+
started_at: "2026-07-12T10:00:00Z",
|
|
138
|
+
finished_at: undefined,
|
|
139
|
+
}));
|
|
140
|
+
const result = getTaskSummary("task-running-001");
|
|
141
|
+
assert.equal(result.terminal, false);
|
|
142
|
+
assert.equal(result.status, "running");
|
|
143
|
+
assert.equal(result.phase, "executing_agent");
|
|
144
|
+
assert.equal(result.acceptance_status, "pending");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|