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,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control Center routes — audit aggregation, warnings, and log tails.
|
|
3
|
+
*
|
|
4
|
+
* `handleAudit` joins independent-review.md + audit.json across tasks and
|
|
5
|
+
* direct sessions. `handleWarnings` buckets audit/stale/verification warnings
|
|
6
|
+
* by type. `handleLogs` returns redacted stdout/stderr tails for core, direct,
|
|
7
|
+
* watcher, and control-center log categories.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { listTasks } from "../../tools/listTasks.js";
|
|
12
|
+
import { redactSensitiveContent } from "../../security/contentRedaction.js";
|
|
13
|
+
import { getDirectSessionsDir, getTasksDir } from "../../config.js";
|
|
14
|
+
import { classifyStaleTask, fileMtimeIso, parseReviewVerdict, readWatcherStatusSafe, } from "../runtime.js";
|
|
15
|
+
import { config, errorMessage, findLatestLog, getControlCenterLogDir, getRuntimeRoot, readFileTail, readJsonFileSafe, readTextFileSafe, sendJson, } from "../shared.js";
|
|
16
|
+
export function handleLogs(res, category, tailLines) {
|
|
17
|
+
try {
|
|
18
|
+
let dir;
|
|
19
|
+
let stdoutPath;
|
|
20
|
+
let stderrPath;
|
|
21
|
+
let stdoutExists;
|
|
22
|
+
let stderrExists;
|
|
23
|
+
if (category === "control-center") {
|
|
24
|
+
dir = getControlCenterLogDir();
|
|
25
|
+
stdoutPath = join(dir, "control-center.stdout.log");
|
|
26
|
+
stderrPath = join(dir, "control-center.stderr.log");
|
|
27
|
+
stdoutExists = existsSync(stdoutPath);
|
|
28
|
+
stderrExists = existsSync(stderrPath);
|
|
29
|
+
}
|
|
30
|
+
else if (category === "watcher") {
|
|
31
|
+
dir = getRuntimeRoot(false);
|
|
32
|
+
const sp = findLatestLog(dir, /^watcher-.*\.stdout\.log$/);
|
|
33
|
+
const ep = findLatestLog(dir, /^watcher-.*\.stderr\.log$/);
|
|
34
|
+
stdoutPath = sp ?? "";
|
|
35
|
+
stderrPath = ep ?? "";
|
|
36
|
+
stdoutExists = sp !== null;
|
|
37
|
+
stderrExists = ep !== null;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// core | direct -> tunnel client logs in the matching runtime dir
|
|
41
|
+
dir = getRuntimeRoot(category === "direct");
|
|
42
|
+
stdoutPath = join(dir, "tunnel-client.stdout.log");
|
|
43
|
+
stderrPath = join(dir, "tunnel-client.stderr.log");
|
|
44
|
+
stdoutExists = existsSync(stdoutPath);
|
|
45
|
+
stderrExists = existsSync(stderrPath);
|
|
46
|
+
}
|
|
47
|
+
if (!stdoutExists && !stderrExists) {
|
|
48
|
+
sendJson(res, 200, {
|
|
49
|
+
stdout: "",
|
|
50
|
+
stderr: "",
|
|
51
|
+
category,
|
|
52
|
+
tail: tailLines,
|
|
53
|
+
reason: "log file not found",
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const stdoutRaw = stdoutExists ? readFileTail(stdoutPath, tailLines) : "";
|
|
58
|
+
const stderrRaw = stderrExists ? readFileTail(stderrPath, tailLines) : "";
|
|
59
|
+
const stdout = redactSensitiveContent(stdoutRaw).content;
|
|
60
|
+
const stderr = redactSensitiveContent(stderrRaw).content;
|
|
61
|
+
sendJson(res, 200, { stdout, stderr, category, tail: tailLines, reason: null });
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
sendJson(res, 200, { stdout: "", stderr: "", category, tail: tailLines, reason: errorMessage(err) });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function handleAudit(res) {
|
|
68
|
+
try {
|
|
69
|
+
const audits = [];
|
|
70
|
+
// 1. tasks/*/independent-review.md (written by audit_task — the primary audit artifact)
|
|
71
|
+
// 2. tasks/*/audit.json (legacy/explicit JSON audit, if present)
|
|
72
|
+
const tasksDir = getTasksDir(config);
|
|
73
|
+
if (existsSync(tasksDir)) {
|
|
74
|
+
let taskEntries = [];
|
|
75
|
+
try {
|
|
76
|
+
taskEntries = readdirSync(tasksDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
taskEntries = [];
|
|
80
|
+
}
|
|
81
|
+
for (const entry of taskEntries) {
|
|
82
|
+
const taskDir = join(tasksDir, entry.name);
|
|
83
|
+
// independent-review.md
|
|
84
|
+
const reviewFile = join(taskDir, "independent-review.md");
|
|
85
|
+
if (existsSync(reviewFile)) {
|
|
86
|
+
const content = readTextFileSafe(reviewFile) ?? "";
|
|
87
|
+
audits.push({
|
|
88
|
+
task_id: entry.name,
|
|
89
|
+
source: "independent-review.md",
|
|
90
|
+
verdict: parseReviewVerdict(content),
|
|
91
|
+
checked_at: fileMtimeIso(reviewFile),
|
|
92
|
+
content_excerpt: content.slice(0, 500),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// audit.json (explicit JSON audit if present)
|
|
96
|
+
const auditFile = join(taskDir, "audit.json");
|
|
97
|
+
if (existsSync(auditFile)) {
|
|
98
|
+
const data = readJsonFileSafe(auditFile);
|
|
99
|
+
if (data) {
|
|
100
|
+
audits.push({
|
|
101
|
+
task_id: entry.name,
|
|
102
|
+
source: "audit.json",
|
|
103
|
+
checked_at: data.checked_at ?? fileMtimeIso(auditFile),
|
|
104
|
+
...data,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// 3. direct-sessions/*/audit.json (written by Direct audit_session)
|
|
111
|
+
const sessionsDir = getDirectSessionsDir(config);
|
|
112
|
+
if (existsSync(sessionsDir)) {
|
|
113
|
+
let sessionEntries = [];
|
|
114
|
+
try {
|
|
115
|
+
sessionEntries = readdirSync(sessionsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
sessionEntries = [];
|
|
119
|
+
}
|
|
120
|
+
for (const entry of sessionEntries) {
|
|
121
|
+
const auditFile = join(sessionsDir, entry.name, "audit.json");
|
|
122
|
+
if (!existsSync(auditFile))
|
|
123
|
+
continue;
|
|
124
|
+
const data = readJsonFileSafe(auditFile);
|
|
125
|
+
if (data) {
|
|
126
|
+
audits.push({
|
|
127
|
+
source: "direct-session",
|
|
128
|
+
session_id: data.session_id ?? entry.name,
|
|
129
|
+
checked_at: fileMtimeIso(auditFile),
|
|
130
|
+
...data,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Sort by checked_at descending (missing timestamps sort last).
|
|
136
|
+
audits.sort((a, b) => {
|
|
137
|
+
const ac = String(a.checked_at ?? "");
|
|
138
|
+
const bc = String(b.checked_at ?? "");
|
|
139
|
+
return bc.localeCompare(ac);
|
|
140
|
+
});
|
|
141
|
+
const limited = audits.slice(0, 50);
|
|
142
|
+
sendJson(res, 200, { audits: limited, total: limited.length });
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
sendJson(res, 200, { audits: [], reason: errorMessage(err) });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Aggregate warnings from multiple sources:
|
|
150
|
+
* - audit.json warnings/fail_checks/possible_false_positives/manual_verification_required
|
|
151
|
+
* - stale task classification (heartbeat stale, collecting_artifacts stale, etc.)
|
|
152
|
+
* - task status=failed_verification
|
|
153
|
+
*
|
|
154
|
+
* Each warning type is grouped into a bucket with affected task IDs. The
|
|
155
|
+
* handler is fault-tolerant: any per-task read failure is skipped, and a
|
|
156
|
+
* top-level failure returns an empty warnings array (never 500).
|
|
157
|
+
*/
|
|
158
|
+
export function handleWarnings(res) {
|
|
159
|
+
try {
|
|
160
|
+
const buckets = {
|
|
161
|
+
unrecorded_command_execution: new Set(),
|
|
162
|
+
artifact_hygiene: new Set(),
|
|
163
|
+
scope_changes: new Set(),
|
|
164
|
+
release_publish_claim: new Set(),
|
|
165
|
+
manual_verification_required: new Set(),
|
|
166
|
+
stale_task: new Set(),
|
|
167
|
+
failed_verification: new Set(),
|
|
168
|
+
};
|
|
169
|
+
const tasksDir = getTasksDir(config);
|
|
170
|
+
let taskList = [];
|
|
171
|
+
let watcher;
|
|
172
|
+
try {
|
|
173
|
+
const result = listTasks({ limit: 100 });
|
|
174
|
+
taskList = result.tasks;
|
|
175
|
+
watcher = result.watcher;
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
watcher = readWatcherStatusSafe();
|
|
179
|
+
}
|
|
180
|
+
const now = Date.now();
|
|
181
|
+
for (const task of taskList) {
|
|
182
|
+
const taskId = task.task_id;
|
|
183
|
+
// 1. Read audit.json for warning strings
|
|
184
|
+
const auditFile = join(tasksDir, taskId, "audit.json");
|
|
185
|
+
const audit = readJsonFileSafe(auditFile);
|
|
186
|
+
if (audit) {
|
|
187
|
+
const warningTexts = [];
|
|
188
|
+
const collectStrings = (field) => {
|
|
189
|
+
if (Array.isArray(field)) {
|
|
190
|
+
for (const w of field) {
|
|
191
|
+
if (typeof w === "string") {
|
|
192
|
+
warningTexts.push(w);
|
|
193
|
+
}
|
|
194
|
+
else if (w && typeof w === "object") {
|
|
195
|
+
const obj = w;
|
|
196
|
+
if (typeof obj.message === "string")
|
|
197
|
+
warningTexts.push(obj.message);
|
|
198
|
+
else if (typeof obj.description === "string")
|
|
199
|
+
warningTexts.push(obj.description);
|
|
200
|
+
else if (typeof obj.warning === "string")
|
|
201
|
+
warningTexts.push(obj.warning);
|
|
202
|
+
else
|
|
203
|
+
warningTexts.push(JSON.stringify(obj));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else if (typeof field === "string") {
|
|
208
|
+
warningTexts.push(field);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
collectStrings(audit.warnings);
|
|
212
|
+
collectStrings(audit.fail_checks);
|
|
213
|
+
collectStrings(audit.possible_false_positives);
|
|
214
|
+
for (const text of warningTexts) {
|
|
215
|
+
const lower = text.toLowerCase();
|
|
216
|
+
if (lower.includes("unrecorded") || lower.includes("command execution")) {
|
|
217
|
+
buckets.unrecorded_command_execution.add(taskId);
|
|
218
|
+
}
|
|
219
|
+
if (lower.includes("artifact") || lower.includes("hygiene")) {
|
|
220
|
+
buckets.artifact_hygiene.add(taskId);
|
|
221
|
+
}
|
|
222
|
+
if (lower.includes("scope") || lower.includes("out_of_scope")) {
|
|
223
|
+
buckets.scope_changes.add(taskId);
|
|
224
|
+
}
|
|
225
|
+
if (lower.includes("release") || lower.includes("publish")) {
|
|
226
|
+
buckets.release_publish_claim.add(taskId);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// manual_verification_required flag
|
|
230
|
+
if (audit.manual_verification_required === true) {
|
|
231
|
+
buckets.manual_verification_required.add(taskId);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// 2. Stale tasks
|
|
235
|
+
try {
|
|
236
|
+
const cls = classifyStaleTask(task, watcher, now);
|
|
237
|
+
if (cls.is_stale) {
|
|
238
|
+
buckets.stale_task.add(taskId);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// skip stale classification failure
|
|
243
|
+
}
|
|
244
|
+
// 3. Failed verification
|
|
245
|
+
if (task.status === "failed_verification") {
|
|
246
|
+
buckets.failed_verification.add(taskId);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const warnings = [];
|
|
250
|
+
const addWarning = (type, severity, likelyFalsePositive, needsFix, blocked, recommendedAction) => {
|
|
251
|
+
const tasks = buckets[type];
|
|
252
|
+
if (tasks.size === 0)
|
|
253
|
+
return;
|
|
254
|
+
warnings.push({
|
|
255
|
+
type,
|
|
256
|
+
severity,
|
|
257
|
+
affected_tasks_count: tasks.size,
|
|
258
|
+
affected_tasks: Array.from(tasks),
|
|
259
|
+
likely_false_positive: likelyFalsePositive,
|
|
260
|
+
needs_fix: needsFix,
|
|
261
|
+
blocked,
|
|
262
|
+
recommended_action: recommendedAction,
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
addWarning("failed_verification", "error", false, true, false, "Re-run verification or fix the failing checks before retrying.");
|
|
266
|
+
addWarning("stale_task", "warning", false, false, false, "Reconcile or recreate stale tasks.");
|
|
267
|
+
addWarning("manual_verification_required", "warning", false, false, false, "Review the audit findings and manually verify the changes.");
|
|
268
|
+
addWarning("unrecorded_command_execution", "info", false, true, false, "Investigate unrecorded command executions; ensure watcher captures all commands.");
|
|
269
|
+
addWarning("release_publish_claim", "info", false, false, true, "Verify the release/publish claim against actual release artifacts before proceeding.");
|
|
270
|
+
addWarning("artifact_hygiene", "info", true, false, false, "Review artifact hygiene warnings; many are likely false positives.");
|
|
271
|
+
addWarning("scope_changes", "info", true, false, false, "Review scope changes; verify they are intentional before accepting.");
|
|
272
|
+
sendJson(res, 200, { warnings, total: warnings.length });
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
sendJson(res, 200, { warnings: [], total: 0, error: errorMessage(err) });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ServerResponse } from "node:http";
|
|
2
|
+
export declare function handleEvidencePacks(res: ServerResponse): void;
|
|
3
|
+
export declare function handleEvidencePackDetail(res: ServerResponse, lineageId: string): void;
|
|
4
|
+
export declare function handleEvidencePackExport(res: ServerResponse, lineageId: string): void;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control Center routes — evidence packs (/api/evidence-packs/*).
|
|
3
|
+
*
|
|
4
|
+
* Lists exported evidence packs (bounded to 50), reads a single pack detail,
|
|
5
|
+
* and exports a pack for a lineage on demand. The export endpoint is a POST
|
|
6
|
+
* route gated by the control token in the server router.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { exportTaskEvidencePack, listEvidencePacks, readEvidencePack, } from "../../tools/evidencePack.js";
|
|
11
|
+
import { errorMessage, sendJson, config } from "../shared.js";
|
|
12
|
+
function augmentEvidencePackSummary(pack) {
|
|
13
|
+
const jsonExists = existsSync(pack.files.json);
|
|
14
|
+
const mdExists = existsSync(pack.files.markdown);
|
|
15
|
+
return {
|
|
16
|
+
...pack,
|
|
17
|
+
export_status: jsonExists ? "exported" : "pending",
|
|
18
|
+
evidence_json_exists: jsonExists,
|
|
19
|
+
evidence_md_exists: mdExists,
|
|
20
|
+
exported_at: pack.generated_at,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function handleEvidencePacks(res) {
|
|
24
|
+
try {
|
|
25
|
+
const list = listEvidencePacks({ max_items: 50 });
|
|
26
|
+
const packs = list.evidence_packs.map((pack) => augmentEvidencePackSummary(pack));
|
|
27
|
+
// Detect lineages that exist but have no exported evidence pack yet, so the
|
|
28
|
+
// dashboard can show an "Export evidence pack" action for the most recent one.
|
|
29
|
+
const lineagesRoot = join(config.workspaceRoot, ".patchwarden", "lineages");
|
|
30
|
+
let lineageCount = 0;
|
|
31
|
+
const exportedIds = new Set(packs.map((p) => p.lineage_id));
|
|
32
|
+
const pendingLineageIds = [];
|
|
33
|
+
if (existsSync(lineagesRoot)) {
|
|
34
|
+
const dirs = readdirSync(lineagesRoot, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
35
|
+
lineageCount = dirs.length;
|
|
36
|
+
for (const dir of dirs) {
|
|
37
|
+
if (!exportedIds.has(dir.name))
|
|
38
|
+
pendingLineageIds.push(dir.name);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
sendJson(res, 200, {
|
|
42
|
+
evidence_packs: packs,
|
|
43
|
+
total: packs.length,
|
|
44
|
+
truncated: list.truncated,
|
|
45
|
+
has_lineages: lineageCount > 0,
|
|
46
|
+
lineage_count: lineageCount,
|
|
47
|
+
pending_lineage_ids: pendingLineageIds.slice(0, 20),
|
|
48
|
+
reason: null,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
sendJson(res, 200, { evidence_packs: [], total: 0, reason: errorMessage(err) });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function handleEvidencePackDetail(res, lineageId) {
|
|
56
|
+
try {
|
|
57
|
+
if (!/^[A-Za-z0-9_-]+$/.test(lineageId)) {
|
|
58
|
+
sendJson(res, 400, { error: "Invalid lineage id" });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const pack = readEvidencePack(lineageId);
|
|
62
|
+
if (!pack) {
|
|
63
|
+
sendJson(res, 404, { error: "Evidence pack not found" });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
sendJson(res, 200, augmentEvidencePackSummary(pack));
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
sendJson(res, 200, { lineage_id: lineageId, error: errorMessage(err), bounded: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function handleEvidencePackExport(res, lineageId) {
|
|
73
|
+
try {
|
|
74
|
+
if (!/^[A-Za-z0-9_-]+$/.test(lineageId)) {
|
|
75
|
+
sendJson(res, 400, { error: "Invalid lineage id" });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Check lineage exists.
|
|
79
|
+
const lineageFile = join(config.workspaceRoot, ".patchwarden", "lineages", lineageId, "lineage.json");
|
|
80
|
+
if (!existsSync(lineageFile)) {
|
|
81
|
+
sendJson(res, 404, { error: "Lineage not found" });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Check if already exported.
|
|
85
|
+
const evidenceJsonPath = join(config.workspaceRoot, ".patchwarden", "evidence-packs", lineageId, "evidence.json");
|
|
86
|
+
if (existsSync(evidenceJsonPath)) {
|
|
87
|
+
sendJson(res, 200, { ok: true, already_exported: true, lineage_id: lineageId });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const pack = exportTaskEvidencePack({ lineage_id: lineageId });
|
|
91
|
+
sendJson(res, 200, { ok: true, exported: true, lineage_id: lineageId, evidence_pack_id: pack.evidence_pack_id });
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
sendJson(res, 200, { ok: false, lineage_id: lineageId, error: errorMessage(err) });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control Center routes — task lineage (/api/lineages/*).
|
|
3
|
+
*
|
|
4
|
+
* Lists all lineages under .patchwarden/lineages (bounded to 50, most recently
|
|
5
|
+
* updated first) and serves a single lineage detail. Each record is projected
|
|
6
|
+
* through `toSafeTaskLineage` so full artifact content is bounded.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { toSafeTaskLineage } from "../../tools/taskLineage.js";
|
|
11
|
+
import { config, errorMessage, readJsonFileSafeUnder, sendJson } from "../shared.js";
|
|
12
|
+
function augmentLineageSummary(safe, record) {
|
|
13
|
+
const directSessions = safe.tasks.direct_sessions;
|
|
14
|
+
const firstDirect = directSessions.length > 0 ? directSessions[0] : null;
|
|
15
|
+
const directVerification = firstDirect
|
|
16
|
+
? {
|
|
17
|
+
session_id: firstDirect.session_id,
|
|
18
|
+
status: firstDirect.status || "unknown",
|
|
19
|
+
audit_decision: firstDirect.audit_decision || "not_run",
|
|
20
|
+
command_count: firstDirect.command_count ?? 0,
|
|
21
|
+
passed_commands: firstDirect.passed_commands ?? 0,
|
|
22
|
+
failed_commands: firstDirect.failed_commands ?? 0,
|
|
23
|
+
}
|
|
24
|
+
: null;
|
|
25
|
+
return {
|
|
26
|
+
...safe,
|
|
27
|
+
iterations: record.rounds.length,
|
|
28
|
+
main_task_count: record.main_task ? 1 : 0,
|
|
29
|
+
fix_task_count: record.fix_tasks.length,
|
|
30
|
+
cleanup_task_count: record.cleanup_tasks.length,
|
|
31
|
+
direct_verification: directVerification,
|
|
32
|
+
warnings_count: record.warnings.length,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function handleLineages(res) {
|
|
36
|
+
try {
|
|
37
|
+
const root = join(config.workspaceRoot, ".patchwarden", "lineages");
|
|
38
|
+
if (!existsSync(root)) {
|
|
39
|
+
sendJson(res, 200, { lineages: [], total: 0, reason: null });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const lineages = readdirSync(root, { withFileTypes: true })
|
|
43
|
+
.filter((entry) => entry.isDirectory())
|
|
44
|
+
.map((entry) => readJsonFileSafeUnder(root, join(entry.name, "lineage.json")))
|
|
45
|
+
.filter((entry) => entry !== null)
|
|
46
|
+
.map((entry) => augmentLineageSummary(toSafeTaskLineage(entry, 6), entry))
|
|
47
|
+
.sort((a, b) => b.updated_at.localeCompare(a.updated_at))
|
|
48
|
+
.slice(0, 50);
|
|
49
|
+
sendJson(res, 200, { lineages, total: lineages.length, reason: null });
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
sendJson(res, 200, { lineages: [], total: 0, reason: errorMessage(err) });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function handleLineageDetail(res, lineageId) {
|
|
56
|
+
try {
|
|
57
|
+
if (!/^[A-Za-z0-9_-]+$/.test(lineageId)) {
|
|
58
|
+
sendJson(res, 400, { error: "Invalid lineage id" });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const data = readJsonFileSafeUnder(join(config.workspaceRoot, ".patchwarden", "lineages"), join(lineageId, "lineage.json"));
|
|
62
|
+
if (!data) {
|
|
63
|
+
sendJson(res, 404, { error: "Lineage not found" });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
sendJson(res, 200, augmentLineageSummary(toSafeTaskLineage(data, 20), data));
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
sendJson(res, 200, { lineage_id: lineageId, error: errorMessage(err) });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control Center routes — project policy + release status.
|
|
3
|
+
*
|
|
4
|
+
* Read-only wrappers around `getProjectPolicySummary` that surface release
|
|
5
|
+
* readiness, blocked commands, and version consistency for the dashboard's
|
|
6
|
+
* release panel. Never performs remote writes (`remote_write_performed` is
|
|
7
|
+
* always false).
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { getProjectPolicySummary } from "../../policy/projectPolicy.js";
|
|
12
|
+
import { errorMessage, sendJson } from "../shared.js";
|
|
13
|
+
export function handleProjectPolicy(res, repoPath) {
|
|
14
|
+
try {
|
|
15
|
+
sendJson(res, 200, getProjectPolicySummary(repoPath || "."));
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
sendJson(res, 200, {
|
|
19
|
+
repo_path: repoPath || ".",
|
|
20
|
+
valid: false,
|
|
21
|
+
issues: [{ code: "policy_unavailable", severity: "error", field: "repo_path", message: errorMessage(err) }],
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function handleReleaseStatus(res, repoPath) {
|
|
26
|
+
try {
|
|
27
|
+
const policy = getProjectPolicySummary(repoPath || ".");
|
|
28
|
+
const readiness = policy.release_readiness;
|
|
29
|
+
const requiredCommands = readiness.required_commands.map((c) => ({
|
|
30
|
+
command: c.command,
|
|
31
|
+
allowed: c.allowed,
|
|
32
|
+
blocked_reason: c.allowed ? null : (c.reason || "not_allowed"),
|
|
33
|
+
}));
|
|
34
|
+
const commandsBlockedCount = requiredCommands.filter((c) => !c.allowed).length;
|
|
35
|
+
const hasPackageJson = existsSync(join(policy.resolved_repo_path, "package.json"));
|
|
36
|
+
const versionSource = hasPackageJson
|
|
37
|
+
? "package.json"
|
|
38
|
+
: (repoPath || ".") === "."
|
|
39
|
+
? "workspace_root"
|
|
40
|
+
: "unknown";
|
|
41
|
+
let readyState;
|
|
42
|
+
if (!policy.valid || commandsBlockedCount > 0) {
|
|
43
|
+
readyState = "blocked";
|
|
44
|
+
}
|
|
45
|
+
else if (readiness.version === null) {
|
|
46
|
+
readyState = "unknown";
|
|
47
|
+
}
|
|
48
|
+
else if (readiness.version_consistent === true) {
|
|
49
|
+
readyState = "ready";
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
readyState = "blocked";
|
|
53
|
+
}
|
|
54
|
+
sendJson(res, 200, {
|
|
55
|
+
repo_path: repoPath || ".",
|
|
56
|
+
resolved_repo_path: policy.resolved_repo_path,
|
|
57
|
+
policy_valid: policy.valid,
|
|
58
|
+
policy_issue_count: policy.issues.length,
|
|
59
|
+
policy_issues: policy.issues.slice(0, 10),
|
|
60
|
+
release_readiness: readiness,
|
|
61
|
+
package_name: readiness.package_name,
|
|
62
|
+
version_source: versionSource,
|
|
63
|
+
version_consistent: readiness.version_consistent,
|
|
64
|
+
required_commands: requiredCommands,
|
|
65
|
+
commands_blocked_count: commandsBlockedCount,
|
|
66
|
+
ready_state: readyState,
|
|
67
|
+
next_action: policy.valid && readiness.version_consistent !== false
|
|
68
|
+
? "Run release_check via create_task template, or run_task_loop with template=release_check."
|
|
69
|
+
: "Fix project-policy or version consistency issues before release preparation.",
|
|
70
|
+
remote_write_performed: false,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
sendJson(res, 200, {
|
|
75
|
+
repo_path: repoPath || ".",
|
|
76
|
+
policy_valid: false,
|
|
77
|
+
error: errorMessage(err),
|
|
78
|
+
remote_write_performed: false,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type ServerResponse } from "node:http";
|
|
2
|
+
declare function findTunnelClientExecutable(): string | null;
|
|
3
|
+
export { findTunnelClientExecutable };
|
|
4
|
+
export declare function handleManageAction(res: ServerResponse, action: string, mode: string): Promise<void>;
|
|
5
|
+
export declare function handleOpenLogsFolder(res: ServerResponse): void;
|