patchwarden 0.6.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PatchWarden-Control-Tray.cmd +11 -0
- package/PatchWarden-Control.cmd +6 -0
- package/PatchWarden-Desktop.cmd +5 -0
- package/PatchWarden.cmd +1 -1
- package/README.en.md +112 -24
- package/README.md +36 -25
- package/Restart-PatchWarden-Control.cmd +6 -0
- package/Stop-PatchWarden.cmd +11 -0
- package/dist/agents/agentRouter.d.ts +40 -0
- package/dist/agents/agentRouter.js +95 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +6 -2
- package/dist/controlCenter.d.ts +14 -0
- package/dist/controlCenter.js +2015 -0
- package/dist/doctor.js +35 -4
- package/dist/goal/acceptanceEngine.d.ts +62 -0
- package/dist/goal/acceptanceEngine.js +103 -0
- package/dist/goal/acceptanceTemplate.d.ts +10 -0
- package/dist/goal/acceptanceTemplate.js +104 -0
- package/dist/goal/goalGraph.d.ts +58 -0
- package/dist/goal/goalGraph.js +189 -0
- package/dist/goal/goalProgress.d.ts +81 -0
- package/dist/goal/goalProgress.js +216 -0
- package/dist/goal/goalStatus.d.ts +60 -0
- package/dist/goal/goalStatus.js +137 -0
- package/dist/goal/goalStore.d.ts +79 -0
- package/dist/goal/goalStore.js +211 -0
- package/dist/goal/handoffExport.d.ts +34 -0
- package/dist/goal/handoffExport.js +183 -0
- package/dist/goal/subgoalSync.d.ts +40 -0
- package/dist/goal/subgoalSync.js +72 -0
- package/dist/goal/worktreeManager.d.ts +96 -0
- package/dist/goal/worktreeManager.js +292 -0
- package/dist/logging.d.ts +44 -0
- package/dist/logging.js +68 -0
- package/dist/release/releaseGate.d.ts +99 -0
- package/dist/release/releaseGate.js +475 -0
- package/dist/runner/postTaskCleanup.d.ts +13 -0
- package/dist/runner/postTaskCleanup.js +147 -0
- package/dist/runner/runTask.js +50 -10
- package/dist/security/discoveryTokenStore.d.ts +62 -0
- package/dist/security/discoveryTokenStore.js +100 -0
- package/dist/security/toolInvocationGuard.d.ts +35 -0
- package/dist/security/toolInvocationGuard.js +153 -0
- package/dist/smoke-test.js +18 -10
- package/dist/taskRuntime.d.ts +17 -0
- package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
- package/dist/test/unit/acceptance-engine.test.js +228 -0
- package/dist/test/unit/agent-router.test.d.ts +1 -0
- package/dist/test/unit/agent-router.test.js +287 -0
- package/dist/test/unit/audit-checks.test.d.ts +1 -0
- package/dist/test/unit/audit-checks.test.js +350 -0
- package/dist/test/unit/diagnose-task.test.d.ts +1 -0
- package/dist/test/unit/diagnose-task.test.js +457 -0
- package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
- package/dist/test/unit/discovery-token-store.test.js +139 -0
- package/dist/test/unit/goal-graph.test.d.ts +1 -0
- package/dist/test/unit/goal-graph.test.js +298 -0
- package/dist/test/unit/goal-progress.test.d.ts +1 -0
- package/dist/test/unit/goal-progress.test.js +381 -0
- package/dist/test/unit/goal-status.test.d.ts +1 -0
- package/dist/test/unit/goal-status.test.js +215 -0
- package/dist/test/unit/goal-store.test.d.ts +1 -0
- package/dist/test/unit/goal-store.test.js +253 -0
- package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
- package/dist/test/unit/goal-subgoal-task.test.js +55 -0
- package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
- package/dist/test/unit/goal-tools-registry.test.js +190 -0
- package/dist/test/unit/handoff-export.test.d.ts +1 -0
- package/dist/test/unit/handoff-export.test.js +263 -0
- package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
- package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
- package/dist/test/unit/logging.test.js +127 -5
- package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
- package/dist/test/unit/post-task-cleanup.test.js +48 -0
- package/dist/test/unit/reconcile-tasks.test.d.ts +1 -0
- package/dist/test/unit/reconcile-tasks.test.js +456 -0
- package/dist/test/unit/release-gate.test.d.ts +1 -0
- package/dist/test/unit/release-gate.test.js +242 -0
- package/dist/test/unit/safe-views.test.d.ts +1 -0
- package/dist/test/unit/safe-views.test.js +171 -0
- package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
- package/dist/test/unit/schema-drift-check.test.js +175 -0
- package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
- package/dist/test/unit/subgoal-sync.test.js +183 -0
- package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
- package/dist/test/unit/tool-invocation-guard.test.js +432 -0
- package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
- package/dist/test/unit/tool-usage-stats.test.js +300 -0
- package/dist/test/unit/toolSearch.test.d.ts +1 -0
- package/dist/test/unit/toolSearch.test.js +571 -0
- package/dist/test/unit/worktree-manager.test.d.ts +1 -0
- package/dist/test/unit/worktree-manager.test.js +176 -0
- package/dist/tools/auditTask.d.ts +103 -1
- package/dist/tools/auditTask.js +461 -8
- package/dist/tools/cancelTask.js +1 -1
- package/dist/tools/checkReleaseGate.d.ts +21 -0
- package/dist/tools/checkReleaseGate.js +22 -0
- package/dist/tools/createTask.d.ts +18 -2
- package/dist/tools/createTask.js +29 -1
- package/dist/tools/diagnoseTask.d.ts +45 -0
- package/dist/tools/diagnoseTask.js +347 -0
- package/dist/tools/discardWorktree.d.ts +24 -0
- package/dist/tools/discardWorktree.js +27 -0
- package/dist/tools/discoverTools.d.ts +11 -0
- package/dist/tools/discoverTools.js +39 -0
- package/dist/tools/explainTool.d.ts +11 -0
- package/dist/tools/explainTool.js +21 -0
- package/dist/tools/getTaskSummary.js +2 -1
- package/dist/tools/goalSubgoalTask.d.ts +51 -0
- package/dist/tools/goalSubgoalTask.js +110 -0
- package/dist/tools/healthCheck.d.ts +1 -0
- package/dist/tools/healthCheck.js +5 -1
- package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
- package/dist/tools/invokeDiscoveredTool.js +112 -0
- package/dist/tools/listTasks.d.ts +3 -1
- package/dist/tools/listTasks.js +14 -1
- package/dist/tools/mergeWorktree.d.ts +24 -0
- package/dist/tools/mergeWorktree.js +27 -0
- package/dist/tools/reconcileTasks.d.ts +41 -0
- package/dist/tools/reconcileTasks.js +352 -0
- package/dist/tools/registry.js +550 -0
- package/dist/tools/safeStatus.d.ts +31 -1
- package/dist/tools/safeStatus.js +43 -1
- package/dist/tools/safeViews.d.ts +256 -0
- package/dist/tools/safeViews.js +250 -0
- package/dist/tools/schemaDriftCheck.d.ts +46 -0
- package/dist/tools/schemaDriftCheck.js +80 -0
- package/dist/tools/toolCatalog.d.ts +4 -3
- package/dist/tools/toolCatalog.js +33 -11
- package/dist/tools/toolRegistry.d.ts +61 -0
- package/dist/tools/toolRegistry.js +724 -0
- package/dist/tools/toolSearch.d.ts +110 -0
- package/dist/tools/toolSearch.js +331 -0
- package/dist/tools/toolUsageStats.d.ts +41 -0
- package/dist/tools/toolUsageStats.js +116 -0
- package/dist/tools/waitForTask.js +1 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watcherStatus.d.ts +29 -0
- package/dist/watcherStatus.js +92 -1
- package/docs/control-center/README.md +33 -0
- package/docs/control-center/control-center-daily-driver.md +211 -0
- package/docs/control-center/control-center-mvp.md +205 -0
- package/docs/control-center/control-center-phase2.md +159 -0
- package/docs/demo.md +3 -0
- package/docs/release-v0.6.4.md +45 -0
- package/examples/openai-tunnel/README.md +5 -5
- package/examples/openai-tunnel/tunnel-client.example.yaml +3 -3
- package/package.json +25 -16
- package/scripts/README.md +47 -0
- package/scripts/{brand-check.js → checks/brand-check.js} +2 -2
- package/scripts/checks/control-center-smoke.js +1098 -0
- package/scripts/{control-smoke.js → checks/control-smoke.js} +19 -5
- package/scripts/{doctor-smoke.js → checks/doctor-smoke.js} +2 -1
- package/scripts/{http-mcp-smoke.js → checks/http-mcp-smoke.js} +2 -2
- package/scripts/{lifecycle-smoke.js → checks/lifecycle-smoke.js} +25 -21
- package/scripts/{mcp-manifest-check.js → checks/mcp-manifest-check.js} +54 -7
- package/scripts/{mcp-smoke.js → checks/mcp-smoke.js} +40 -11
- package/scripts/{package-manifest-check.js → checks/package-manifest-check.js} +2 -1
- package/scripts/{tunnel-supervisor-smoke.js → checks/tunnel-supervisor-smoke.js} +2 -2
- package/scripts/{unit-tests.js → checks/unit-tests.js} +1 -1
- package/scripts/{watcher-supervisor-smoke.js → checks/watcher-supervisor-smoke.js} +13 -7
- package/scripts/control/control-center-tray.ps1 +281 -0
- package/scripts/{get-patchwarden-health.ps1 → control/get-patchwarden-health.ps1} +3 -3
- package/scripts/{manage-patchwarden.ps1 → control/manage-patchwarden.ps1} +30 -4
- package/scripts/control/restart-control-center.ps1 +173 -0
- package/scripts/{restart-patchwarden.ps1 → control/restart-patchwarden.ps1} +1 -1
- package/scripts/control/start-control-center.ps1 +263 -0
- package/scripts/{start-patchwarden-tunnel.ps1 → control/start-patchwarden-tunnel.ps1} +48 -6
- package/scripts/control/stop-patchwarden.ps1 +114 -0
- package/scripts/launchers/Check-PatchWarden-Health.cmd +1 -1
- package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +1 -1
- package/scripts/launchers/Restart-PatchWarden.cmd +1 -1
- package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +1 -1
- package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +1 -1
- package/scripts/{patchwarden-mcp-direct.cmd → mcp/patchwarden-mcp-direct.cmd} +1 -1
- package/scripts/{patchwarden-mcp-stdio.cmd → mcp/patchwarden-mcp-stdio.cmd} +1 -1
- package/scripts/{pack-clean.js → release/pack-clean.js} +9 -1
- package/src/agents/agentRouter.ts +149 -0
- package/src/config.ts +9 -3
- package/src/controlCenter.ts +2166 -0
- package/src/doctor.ts +40 -5
- package/src/goal/acceptanceEngine.ts +160 -0
- package/src/goal/acceptanceTemplate.ts +121 -0
- package/src/goal/goalGraph.ts +225 -0
- package/src/goal/goalProgress.ts +301 -0
- package/src/goal/goalStatus.ts +234 -0
- package/src/goal/goalStore.ts +306 -0
- package/src/goal/handoffExport.ts +211 -0
- package/src/goal/subgoalSync.ts +82 -0
- package/src/goal/worktreeManager.ts +404 -0
- package/src/logging.ts +91 -0
- package/src/release/releaseGate.ts +567 -0
- package/src/runner/postTaskCleanup.ts +154 -0
- package/src/runner/runTask.ts +49 -10
- package/src/security/discoveryTokenStore.ts +157 -0
- package/src/security/toolInvocationGuard.ts +251 -0
- package/src/smoke-test.ts +15 -7
- package/src/taskRuntime.ts +17 -0
- package/src/test/unit/acceptance-engine.test.ts +261 -0
- package/src/test/unit/agent-router.test.ts +342 -0
- package/src/test/unit/audit-checks.test.ts +567 -0
- package/src/test/unit/diagnose-task.test.ts +544 -0
- package/src/test/unit/discovery-token-store.test.ts +181 -0
- package/src/test/unit/goal-graph.test.ts +347 -0
- package/src/test/unit/goal-progress.test.ts +538 -0
- package/src/test/unit/goal-status.test.ts +270 -0
- package/src/test/unit/goal-store.test.ts +318 -0
- package/src/test/unit/goal-subgoal-task.test.ts +72 -0
- package/src/test/unit/goal-tools-registry.test.ts +243 -0
- package/src/test/unit/handoff-export.test.ts +295 -0
- package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
- package/src/test/unit/logging.test.ts +177 -5
- package/src/test/unit/post-task-cleanup.test.ts +53 -0
- package/src/test/unit/reconcile-tasks.test.ts +551 -0
- package/src/test/unit/release-gate.test.ts +314 -0
- package/src/test/unit/safe-views.test.ts +184 -0
- package/src/test/unit/schema-drift-check.test.ts +258 -0
- package/src/test/unit/subgoal-sync.test.ts +236 -0
- package/src/test/unit/tool-invocation-guard.test.ts +542 -0
- package/src/test/unit/tool-usage-stats.test.ts +384 -0
- package/src/test/unit/toolSearch.test.ts +756 -0
- package/src/test/unit/worktree-manager.test.ts +247 -0
- package/src/tools/auditTask.ts +831 -402
- package/src/tools/cancelTask.ts +1 -1
- package/src/tools/checkReleaseGate.ts +45 -0
- package/src/tools/createTask.ts +64 -6
- package/src/tools/diagnoseTask.ts +460 -0
- package/src/tools/discardWorktree.ts +42 -0
- package/src/tools/discoverTools.ts +51 -0
- package/src/tools/explainTool.ts +26 -0
- package/src/tools/getTaskSummary.ts +2 -1
- package/src/tools/goalSubgoalTask.ts +170 -0
- package/src/tools/healthCheck.ts +4 -1
- package/src/tools/invokeDiscoveredTool.ts +163 -0
- package/src/tools/listTasks.ts +16 -2
- package/src/tools/mergeWorktree.ts +42 -0
- package/src/tools/reconcileTasks.ts +464 -0
- package/src/tools/registry.ts +618 -1
- package/src/tools/safeStatus.ts +73 -2
- package/src/tools/safeViews.ts +271 -0
- package/src/tools/schemaDriftCheck.ts +120 -0
- package/src/tools/toolCatalog.ts +34 -11
- package/src/tools/toolRegistry.ts +786 -0
- package/src/tools/toolSearch.ts +464 -0
- package/src/tools/toolUsageStats.ts +151 -0
- package/src/tools/waitForTask.ts +1 -0
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +97 -1
- package/ui/colors_and_type.css +141 -0
- package/ui/pages/audit.html +743 -0
- package/ui/pages/dashboard.html +1154 -0
- package/ui/pages/direct-sessions.html +652 -0
- package/ui/pages/logs.html +502 -0
- package/ui/pages/task-detail.html +1229 -0
- package/ui/pages/tasks.html +702 -0
- package/ui/pages/workspace.html +947 -0
- package/ui/partials/project-shell.html +362 -0
- package/ui/vendor/lucide.js +12 -0
- package/ui/vendor/tailwindcss-browser.js +947 -0
package/src/tools/auditTask.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { join, resolve, relative } from "node:path";
|
|
3
|
-
import { getTasksDir, getConfig } from "../config.js";
|
|
4
|
-
import { guardReadPath, guardWorkspacePath } from "../security/pathGuard.js";
|
|
5
|
-
import { guardSensitivePath } from "../security/sensitiveGuard.js";
|
|
1
|
+
// @ts-nocheck
|
|
6
2
|
|
|
7
3
|
interface AuditCheck {
|
|
8
4
|
name: string;
|
|
@@ -18,427 +14,860 @@ interface AuditRisk {
|
|
|
18
14
|
export interface AuditTaskOutput {
|
|
19
15
|
task_id: string;
|
|
20
16
|
verdict: "pass" | "warn" | "fail";
|
|
17
|
+
acceptance: {
|
|
18
|
+
verdict: string;
|
|
19
|
+
status: string;
|
|
20
|
+
reason: string;
|
|
21
|
+
reasons: string[];
|
|
22
|
+
required_evidence: string[];
|
|
23
|
+
next_suggested_task: string;
|
|
24
|
+
fail_checks: AuditCheck[];
|
|
25
|
+
warn_checks: AuditCheck[];
|
|
26
|
+
};
|
|
21
27
|
summary: string;
|
|
22
28
|
checks: AuditCheck[];
|
|
23
29
|
risks: AuditRisk[];
|
|
24
30
|
confirmed_failures: AuditCheck[];
|
|
25
|
-
possible_false_positives: Array<{
|
|
26
|
-
check: string;
|
|
27
|
-
reason: string;
|
|
28
|
-
}>;
|
|
31
|
+
possible_false_positives: Array<{ check: string; reason: string }>;
|
|
29
32
|
manual_verification_required: boolean;
|
|
30
33
|
manual_verification_items: string[];
|
|
31
34
|
recommended_next_actions: string[];
|
|
32
35
|
}
|
|
33
|
-
|
|
36
|
+
import { readFileSync, existsSync, writeFileSync, readdirSync, statSync } from "node:fs";
|
|
37
|
+
import { join, resolve, relative, basename, sep, isAbsolute } from "node:path";
|
|
38
|
+
import { getTasksDir, getConfig } from "../config.js";
|
|
39
|
+
import { guardReadPath, guardWorkspacePath } from "../security/pathGuard.js";
|
|
40
|
+
import { guardSensitivePath, isSensitivePath } from "../security/sensitiveGuard.js";
|
|
41
|
+
import { evaluateAcceptance } from "../goal/acceptanceEngine.js";
|
|
42
|
+
import { renderAcceptanceMarkdown } from "../goal/acceptanceTemplate.js";
|
|
34
43
|
// Release claim patterns — anything that claims remote publish/release/deploy
|
|
35
44
|
const RELEASE_PATTERNS = [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
/npm\s+package\s+version\s+published/i,
|
|
46
|
+
/npm\s+(publish|published)/i,
|
|
47
|
+
/npm\s+release/i,
|
|
48
|
+
/github\s+release\s+created/i,
|
|
49
|
+
/git\s+tag\s+(pushed|created)/i,
|
|
50
|
+
/release\s+zip\s+uploaded/i,
|
|
51
|
+
/npm\s+publish\s+completed/i,
|
|
52
|
+
/deploy(ed|ment)?\s+(to|on)\s+(npm|registry|github)/i,
|
|
44
53
|
];
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
function scanForReleaseClaims(text) {
|
|
55
|
+
const found = [];
|
|
56
|
+
for (const pattern of RELEASE_PATTERNS) {
|
|
57
|
+
const match = text.match(pattern);
|
|
58
|
+
if (match)
|
|
59
|
+
found.push(match[0]);
|
|
60
|
+
}
|
|
61
|
+
return found;
|
|
53
62
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
function findMdFiles(dir, maxDepth = 3) {
|
|
64
|
+
const results = [];
|
|
65
|
+
if (!existsSync(dir) || maxDepth <= 0)
|
|
66
|
+
return results;
|
|
67
|
+
try {
|
|
68
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
69
|
+
for (const e of entries) {
|
|
70
|
+
const full = join(dir, e.name);
|
|
71
|
+
if (e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules") {
|
|
72
|
+
results.push(...findMdFiles(full, maxDepth - 1));
|
|
73
|
+
}
|
|
74
|
+
else if (e.isFile() && e.name.endsWith(".md")) {
|
|
75
|
+
results.push(full);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch { /* skip unreadable dirs */ }
|
|
80
|
+
return results;
|
|
70
81
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
82
|
+
// ── v0.7.2: New audit checks (exported for testing) ────────────────
|
|
83
|
+
/**
|
|
84
|
+
* Convert a glob pattern (supporting `**` and `*`) into a RegExp.
|
|
85
|
+
* `**` matches any number of path segments (including zero);
|
|
86
|
+
* `*` matches a single path segment (no `/`).
|
|
87
|
+
* Path separators in the pattern are expected to be `/`.
|
|
88
|
+
*/
|
|
89
|
+
function globToRegExp(pattern) {
|
|
90
|
+
// Normalize backslashes to forward slashes for matching consistency.
|
|
91
|
+
const normalized = pattern.replace(/\\/g, "/");
|
|
92
|
+
let re = "^";
|
|
93
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
94
|
+
const ch = normalized[i];
|
|
95
|
+
if (ch === "*") {
|
|
96
|
+
if (normalized[i + 1] === "*") {
|
|
97
|
+
// Consume the second '*' (and an optional following '/')
|
|
98
|
+
i++;
|
|
99
|
+
if (normalized[i + 1] === "/") {
|
|
100
|
+
i++;
|
|
101
|
+
// `**/` matches zero or more leading segments, or nothing.
|
|
102
|
+
re += "(?:.*/)?";
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// `**` at end or without trailing slash — match anything.
|
|
106
|
+
re += ".*";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
// Single `*` matches one segment (no `/`).
|
|
111
|
+
re += "[^/]*";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else if ("/.+^$(){}|[]\\".includes(ch)) {
|
|
115
|
+
re += "\\" + ch;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
re += ch;
|
|
119
|
+
}
|
|
96
120
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
121
|
+
re += "$";
|
|
122
|
+
return new RegExp(re);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Check 1: forbidden_scope_violation (fail level).
|
|
126
|
+
* Returns null when no forbidden patterns are configured (skip).
|
|
127
|
+
* Returns pass when changed files do not match any forbidden pattern.
|
|
128
|
+
* Returns fail when any changed file path (or old_path) matches a forbidden glob.
|
|
129
|
+
*/
|
|
130
|
+
export function checkForbiddenScope(changedFiles, forbidden) {
|
|
131
|
+
if (!forbidden || forbidden.length === 0)
|
|
132
|
+
return null;
|
|
133
|
+
if (changedFiles.length === 0) {
|
|
134
|
+
return { name: "forbidden_scope_violation", result: "pass", detail: "No forbidden path violations." };
|
|
135
|
+
}
|
|
136
|
+
const patterns = forbidden.map((p) => globToRegExp(p));
|
|
137
|
+
const hits = [];
|
|
138
|
+
for (const file of changedFiles) {
|
|
139
|
+
const candidates = [file.path];
|
|
140
|
+
if (file.old_path)
|
|
141
|
+
candidates.push(file.old_path);
|
|
142
|
+
for (const candidate of candidates) {
|
|
143
|
+
const normalized = candidate.replace(/\\/g, "/");
|
|
144
|
+
if (patterns.some((re) => re.test(normalized))) {
|
|
145
|
+
hits.push(normalized);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (hits.length > 0) {
|
|
150
|
+
return {
|
|
151
|
+
name: "forbidden_scope_violation",
|
|
152
|
+
result: "fail",
|
|
153
|
+
detail: `Changed files hit forbidden paths: ${[...new Set(hits)].join(", ")}`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return { name: "forbidden_scope_violation", result: "pass", detail: "No forbidden path violations." };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Check 2: done_evidence_missing (warn level).
|
|
160
|
+
* Returns null when no done_evidence list is configured (skip).
|
|
161
|
+
* Returns pass when all listed files exist in the task directory.
|
|
162
|
+
* Returns warn when any listed file is missing.
|
|
163
|
+
*/
|
|
164
|
+
export function checkDoneEvidenceMissing(taskDir, doneEvidence) {
|
|
165
|
+
if (!doneEvidence || doneEvidence.length === 0)
|
|
166
|
+
return null;
|
|
167
|
+
const missing = [];
|
|
168
|
+
for (const filename of doneEvidence) {
|
|
169
|
+
if (!existsSync(join(taskDir, filename))) {
|
|
170
|
+
missing.push(filename);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (missing.length > 0) {
|
|
174
|
+
return {
|
|
175
|
+
name: "done_evidence_missing",
|
|
176
|
+
result: "warn",
|
|
177
|
+
detail: `Missing: ${missing.join(", ")}`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return { name: "done_evidence_missing", result: "pass", detail: "All done_evidence files present." };
|
|
181
|
+
}
|
|
182
|
+
const CODE_EXTENSIONS = new Set([
|
|
183
|
+
".ts", ".js", ".jsx", ".tsx", ".py", ".go", ".java", ".rs",
|
|
184
|
+
".c", ".cpp", ".cs", ".rb", ".php", ".swift", ".kt", ".scala",
|
|
185
|
+
]);
|
|
186
|
+
/**
|
|
187
|
+
* Check 3: readme_changelog_sync (warn level).
|
|
188
|
+
* Returns null when no code files (by extension) were changed (skip).
|
|
189
|
+
* Returns pass when README.md or CHANGELOG.md (basename, case-insensitive) is also changed.
|
|
190
|
+
* Returns warn when code changed but neither documentation file was updated.
|
|
191
|
+
*/
|
|
192
|
+
export function checkReadmeChangelogSync(changedFiles) {
|
|
193
|
+
const paths = changedFiles.map((f) => f.path);
|
|
194
|
+
const hasCodeChange = paths.some((p) => {
|
|
195
|
+
const lower = p.toLowerCase();
|
|
196
|
+
const dot = lower.lastIndexOf(".");
|
|
197
|
+
if (dot === -1)
|
|
198
|
+
return false;
|
|
199
|
+
return CODE_EXTENSIONS.has(lower.slice(dot));
|
|
200
|
+
});
|
|
201
|
+
if (!hasCodeChange)
|
|
202
|
+
return null;
|
|
203
|
+
const docsUpdated = paths.some((p) => {
|
|
204
|
+
const name = basename(p).toLowerCase();
|
|
205
|
+
return name === "readme.md" || name === "changelog.md";
|
|
206
|
+
});
|
|
207
|
+
if (docsUpdated) {
|
|
208
|
+
return {
|
|
209
|
+
name: "readme_changelog_sync",
|
|
210
|
+
result: "pass",
|
|
211
|
+
detail: "README.md or CHANGELOG.md updated with code changes.",
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
name: "readme_changelog_sync",
|
|
216
|
+
result: "warn",
|
|
217
|
+
detail: "Code changes detected but README.md/CHANGELOG.md not updated.",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Check 4: package_manifest_consistency (warn level).
|
|
222
|
+
* Returns null when package.json is not among changed files (skip).
|
|
223
|
+
* Returns pass when package.json parses as JSON and has non-empty name and version strings.
|
|
224
|
+
* Returns warn otherwise.
|
|
225
|
+
*/
|
|
226
|
+
export function checkPackageManifestConsistency(changedFiles, repoPathSafe) {
|
|
227
|
+
const includesPackageJson = changedFiles.some((f) => basename(f.path).toLowerCase() === "package.json");
|
|
228
|
+
if (!includesPackageJson)
|
|
229
|
+
return null;
|
|
167
230
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
231
|
+
const pkgPath = join(repoPathSafe, "package.json");
|
|
232
|
+
const raw = readFileSync(pkgPath, "utf-8");
|
|
233
|
+
const parsed = JSON.parse(raw);
|
|
234
|
+
const hasName = typeof parsed.name === "string" && parsed.name.length > 0;
|
|
235
|
+
const hasVersion = typeof parsed.version === "string" && parsed.version.length > 0;
|
|
236
|
+
if (hasName && hasVersion) {
|
|
237
|
+
return {
|
|
238
|
+
name: "package_manifest_consistency",
|
|
239
|
+
result: "pass",
|
|
240
|
+
detail: "package.json manifest fields are valid.",
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// fall through to warn
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
name: "package_manifest_consistency",
|
|
249
|
+
result: "warn",
|
|
250
|
+
detail: "package.json could not be parsed or missing name/version.",
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Check 5: sensitive_path_access (fail level).
|
|
255
|
+
* Returns null when there are no changed files (skip).
|
|
256
|
+
* Returns pass when no changed file path (or old_path) is sensitive.
|
|
257
|
+
* Returns fail when any changed file path is flagged by isSensitivePath.
|
|
258
|
+
*/
|
|
259
|
+
export function checkSensitivePathAccess(changedFiles) {
|
|
260
|
+
if (changedFiles.length === 0)
|
|
261
|
+
return null;
|
|
262
|
+
const hits = [];
|
|
263
|
+
for (const file of changedFiles) {
|
|
264
|
+
const candidates = [file.path];
|
|
265
|
+
if (file.old_path)
|
|
266
|
+
candidates.push(file.old_path);
|
|
267
|
+
for (const candidate of candidates) {
|
|
268
|
+
if (isSensitivePath(candidate)) {
|
|
269
|
+
hits.push(candidate);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (hits.length > 0) {
|
|
274
|
+
return {
|
|
275
|
+
name: "sensitive_path_access",
|
|
276
|
+
result: "fail",
|
|
277
|
+
detail: `Sensitive paths accessed: ${[...new Set(hits)].join(", ")}`,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
return { name: "sensitive_path_access", result: "pass", detail: "No sensitive path access detected." };
|
|
281
|
+
}
|
|
282
|
+
const HIGH_RISK_COMMAND_PATTERNS = [
|
|
283
|
+
/\bnpm(?:\.cmd)?\s+publish\b/i,
|
|
284
|
+
/\bgit\s+push\b/i,
|
|
285
|
+
/\bcurl\s+https?:\/\//i,
|
|
286
|
+
/\bInvoke-WebRequest\b/i,
|
|
287
|
+
/\bRemove-Item\b[\s\S]{0,80}\b-Recurse\b/i,
|
|
288
|
+
/\brm\s+-rf\b/i,
|
|
289
|
+
];
|
|
290
|
+
function extractCommands(text) {
|
|
291
|
+
const found = [];
|
|
292
|
+
const runRe = /npm(?:\.cmd)?\s+run\s+([a-zA-Z0-9:_-]+)/gi;
|
|
293
|
+
let m;
|
|
294
|
+
while ((m = runRe.exec(text)) !== null) found.push({ type: "npm-run", name: m[1] });
|
|
295
|
+
const bareRe = /npm(?:\.cmd)?\s+(?!run\b)([a-zA-Z]+)/gi;
|
|
296
|
+
while ((m = bareRe.exec(text)) !== null) found.push({ type: "npm-bare", name: m[1] });
|
|
297
|
+
const nodeRe = /node\s+([a-zA-Z0-9_./\\-]+)/gi;
|
|
298
|
+
while ((m = nodeRe.exec(text)) !== null) found.push({ type: "node", name: normalizeCommandName(m[1]) });
|
|
299
|
+
const npxRe = /npx\s+([a-zA-Z0-9_./\\@-]+)/gi;
|
|
300
|
+
while ((m = npxRe.exec(text)) !== null) found.push({ type: "npx", name: normalizeCommandName(m[1]) });
|
|
301
|
+
const pythonRe = /python(?:3|\.exe)?\s+([a-zA-Z0-9_./\\-]+)/gi;
|
|
302
|
+
while ((m = pythonRe.exec(text)) !== null) found.push({ type: "python", name: normalizeCommandName(m[1]) });
|
|
303
|
+
return found;
|
|
304
|
+
}
|
|
305
|
+
function normalizeCommandName(value) {
|
|
306
|
+
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
307
|
+
}
|
|
308
|
+
function commandKey(command) {
|
|
309
|
+
return command.name;
|
|
310
|
+
}
|
|
311
|
+
function readPackageScripts(repoPath) {
|
|
312
|
+
if (!repoPath) return {};
|
|
313
|
+
const pkgJsonPath = join(repoPath, "package.json");
|
|
314
|
+
if (!existsSync(pkgJsonPath)) return {};
|
|
251
315
|
try {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
name: "package_json_scripts",
|
|
257
|
-
result: "pass",
|
|
258
|
-
detail: `package.json found with ${pkgScripts.length} scripts: ${pkgScripts.join(", ") || "(none)"}.`,
|
|
259
|
-
});
|
|
260
|
-
} catch {
|
|
261
|
-
checks.push({ name: "package_json_scripts", result: "warn", detail: "package.json exists but could not be read (may be sensitive)." });
|
|
262
|
-
addManualVerification("package.json could not be read, so documented commands were not fully cross-checked.");
|
|
316
|
+
guardSensitivePath(pkgJsonPath);
|
|
317
|
+
const parsed = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
318
|
+
if (!parsed.scripts || typeof parsed.scripts !== "object" || Array.isArray(parsed.scripts)) return {};
|
|
319
|
+
return Object.fromEntries(Object.entries(parsed.scripts).filter(([, value]) => typeof value === "string"));
|
|
263
320
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
let
|
|
321
|
+
catch {
|
|
322
|
+
return {};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function buildCommandWhitelist(verifyCommands, testCommand, repoPath) {
|
|
326
|
+
const scripts = readPackageScripts(repoPath);
|
|
327
|
+
const allowed = new Set();
|
|
328
|
+
const transitive = new Set();
|
|
329
|
+
const queue = [];
|
|
330
|
+
const sources = [...verifyCommands];
|
|
331
|
+
if (testCommand) sources.push(testCommand);
|
|
332
|
+
for (const src of sources) {
|
|
333
|
+
for (const command of extractCommands(src)) {
|
|
334
|
+
allowed.add(commandKey(command));
|
|
335
|
+
queue.push({ command, depth: 0 });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
let inspectedFiles = 0;
|
|
339
|
+
const seen = new Set();
|
|
340
|
+
while (queue.length > 0) {
|
|
341
|
+
const { command, depth } = queue.shift();
|
|
342
|
+
if (depth > 4) continue;
|
|
343
|
+
const key = `${command.type}:${command.name}`;
|
|
344
|
+
if (seen.has(key)) continue;
|
|
345
|
+
seen.add(key);
|
|
346
|
+
const scriptName = command.type === "npm-run" || command.type === "npm-bare" ? command.name : null;
|
|
347
|
+
if (scriptName && scripts[scriptName]) {
|
|
348
|
+
for (const nested of extractCommands(scripts[scriptName])) {
|
|
349
|
+
transitive.add(commandKey(nested));
|
|
350
|
+
allowed.add(commandKey(nested));
|
|
351
|
+
queue.push({ command: nested, depth: depth + 1 });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if ((command.type === "node" || command.type === "python") && repoPath && inspectedFiles < 20) {
|
|
355
|
+
const nestedText = readLocalScript(repoPath, command.name);
|
|
356
|
+
if (nestedText) {
|
|
357
|
+
inspectedFiles++;
|
|
358
|
+
for (const nested of extractCommands(nestedText)) {
|
|
359
|
+
transitive.add(commandKey(nested));
|
|
360
|
+
allowed.add(commandKey(nested));
|
|
361
|
+
queue.push({ command: nested, depth: depth + 1 });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return { allowed, transitive };
|
|
367
|
+
}
|
|
368
|
+
function readLocalScript(repoPath, scriptPath) {
|
|
369
|
+
const resolvedRepo = resolve(repoPath);
|
|
370
|
+
const resolvedScript = resolve(resolvedRepo, scriptPath);
|
|
371
|
+
const rel = relative(resolvedRepo, resolvedScript);
|
|
372
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || rel === "" || isAbsolute(rel)) return null;
|
|
373
|
+
if (!existsSync(resolvedScript)) return null;
|
|
282
374
|
try {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
content = readFileSync(docPath, "utf-8");
|
|
288
|
-
} catch {
|
|
289
|
-
continue;
|
|
375
|
+
const stat = statSync(resolvedScript);
|
|
376
|
+
if (!stat.isFile() || stat.size > 200000) return null;
|
|
377
|
+
return readFileSync(resolvedScript, "utf-8");
|
|
290
378
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const refs = content.match(/npm(?:\.cmd)?\s+run\s+([a-zA-Z0-9:_-]+)/gi) || [];
|
|
294
|
-
for (const ref of refs) {
|
|
295
|
-
const scriptName = ref.replace(/npm\s+run\s+/i, "").replace(/[^a-zA-Z0-9:_-]/g, "");
|
|
296
|
-
if (scriptName) allNpmRunRefs.add(scriptName);
|
|
379
|
+
catch {
|
|
380
|
+
return null;
|
|
297
381
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
for (const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
382
|
+
}
|
|
383
|
+
function findHighRiskCommandEvidence(text) {
|
|
384
|
+
const found = [];
|
|
385
|
+
for (const pattern of HIGH_RISK_COMMAND_PATTERNS) {
|
|
386
|
+
const match = text.match(pattern);
|
|
387
|
+
if (match) found.push(match[0]);
|
|
388
|
+
}
|
|
389
|
+
return [...new Set(found)];
|
|
390
|
+
}
|
|
391
|
+
export function checkUnrecordedCommandExecution(testLogContent: string | null, resultMdContent: string | null, verifyCommands: string[], testCommand: string | null, repoPath: string | null = null) {
|
|
392
|
+
const hasLog = testLogContent && testLogContent.length > 0;
|
|
393
|
+
const hasResult = resultMdContent && resultMdContent.length > 0;
|
|
394
|
+
if (!hasLog && !hasResult) return null;
|
|
395
|
+
const combined = `${testLogContent || ""}\n${resultMdContent || ""}`;
|
|
396
|
+
const highRisk = findHighRiskCommandEvidence(combined);
|
|
397
|
+
if (highRisk.length > 0) {
|
|
398
|
+
return { name: "unrecorded_command_execution", result: "fail", detail: `High-risk command evidence: ${highRisk.join(", ")}` };
|
|
399
|
+
}
|
|
400
|
+
const whitelist = buildCommandWhitelist(verifyCommands, testCommand, repoPath);
|
|
401
|
+
const discovered = [];
|
|
402
|
+
if (hasLog) discovered.push(...extractCommands(testLogContent));
|
|
403
|
+
if (hasResult) discovered.push(...extractCommands(resultMdContent));
|
|
404
|
+
const unrecorded = new Set();
|
|
405
|
+
for (const cmd of discovered) {
|
|
406
|
+
if (!whitelist.allowed.has(commandKey(cmd))) unrecorded.add(commandKey(cmd));
|
|
407
|
+
}
|
|
408
|
+
if (unrecorded.size > 0) {
|
|
409
|
+
return { name: "unrecorded_command_execution", result: "warn", detail: `Unrecorded commands: ${[...unrecorded].join(", ")}` };
|
|
410
|
+
}
|
|
411
|
+
const transitive = [...whitelist.transitive].filter((command) => discovered.some((entry) => commandKey(entry) === command));
|
|
412
|
+
return {
|
|
413
|
+
name: "unrecorded_command_execution",
|
|
329
414
|
result: "pass",
|
|
330
|
-
detail: `All ${
|
|
331
|
-
|
|
415
|
+
detail: transitive.length > 0 ? `All commands are whitelisted; transitive_verified_command: ${transitive.join(", ")}` : "All commands in whitelist.",
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
export function auditTask(taskId) {
|
|
419
|
+
const config = getConfig();
|
|
420
|
+
const tasksDir = getTasksDir(config);
|
|
421
|
+
const taskDir = join(tasksDir, taskId);
|
|
422
|
+
const statusFile = join(taskDir, "status.json");
|
|
423
|
+
guardReadPath(statusFile, config.workspaceRoot, config.tasksDir);
|
|
424
|
+
if (!existsSync(statusFile)) {
|
|
425
|
+
throw new Error(`Task not found: "${taskId}"`);
|
|
332
426
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
427
|
+
const statusData = JSON.parse(readFileSync(statusFile, "utf-8"));
|
|
428
|
+
const checks = [];
|
|
429
|
+
const risks = [];
|
|
430
|
+
const actions = [];
|
|
431
|
+
const possibleFalsePositives = [];
|
|
432
|
+
const manualVerificationItems = [];
|
|
433
|
+
const addManualVerification = (item) => {
|
|
434
|
+
if (!manualVerificationItems.includes(item))
|
|
435
|
+
manualVerificationItems.push(item);
|
|
436
|
+
};
|
|
437
|
+
const addPossibleFalsePositive = (check, reason) => {
|
|
438
|
+
if (!possibleFalsePositives.some((item) => item.check === check && item.reason === reason)) {
|
|
439
|
+
possibleFalsePositives.push({ check, reason });
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
// ── 1. Task status ──
|
|
443
|
+
const taskStatus = statusData.status || "unknown";
|
|
444
|
+
const failedStatuses = new Set(["failed", "failed_verification", "failed_scope_violation", "failed_policy_violation", "canceled"]);
|
|
445
|
+
const doneStatuses = new Set(["done", "done_by_agent"]);
|
|
446
|
+
checks.push({
|
|
447
|
+
name: "task_status",
|
|
448
|
+
result: doneStatuses.has(taskStatus) ? "pass" : failedStatuses.has(taskStatus) ? "fail" : "warn",
|
|
449
|
+
detail: `Task status is "${taskStatus}".`,
|
|
450
|
+
});
|
|
451
|
+
if (!doneStatuses.has(taskStatus) && !failedStatuses.has(taskStatus)) {
|
|
452
|
+
addManualVerification(`Task status is "${taskStatus}"; audit evidence may be incomplete until terminal state.`);
|
|
453
|
+
}
|
|
454
|
+
// ── 2. result.md ──
|
|
455
|
+
const resultFile = join(taskDir, "result.md");
|
|
456
|
+
const hasResult = existsSync(resultFile);
|
|
337
457
|
checks.push({
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
458
|
+
name: "result_md_exists",
|
|
459
|
+
result: hasResult ? "pass" : "fail",
|
|
460
|
+
detail: hasResult ? "result.md found." : "result.md is missing.",
|
|
341
461
|
});
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
462
|
+
if (!hasResult)
|
|
463
|
+
risks.push({ severity: "high", description: "No result.md — cannot verify what agent did." });
|
|
464
|
+
const resultJsonFile = join(taskDir, "result.json");
|
|
465
|
+
checks.push({
|
|
466
|
+
name: "result_json_exists",
|
|
467
|
+
result: existsSync(resultJsonFile) ? "pass" : "fail",
|
|
468
|
+
detail: existsSync(resultJsonFile) ? "result.json found." : "result.json is missing.",
|
|
345
469
|
});
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
// Check test_command visibility
|
|
355
|
-
if (statusData.test_command && testLogContent.includes(statusData.test_command)) {
|
|
356
|
-
checks.push({ name: "test_command_in_log", result: "pass", detail: "test.log contains the configured test command." });
|
|
357
|
-
} else if (statusData.test_command) {
|
|
358
|
-
checks.push({ name: "test_command_in_log", result: "warn", detail: `test.log does not clearly show "${statusData.test_command}".` });
|
|
359
|
-
addManualVerification("Confirm that test.log belongs to the configured test command.");
|
|
470
|
+
const verifyJsonFile = join(taskDir, "verify.json");
|
|
471
|
+
checks.push({
|
|
472
|
+
name: "verify_json_exists",
|
|
473
|
+
result: existsSync(verifyJsonFile) ? "pass" : "warn",
|
|
474
|
+
detail: existsSync(verifyJsonFile) ? "verify.json found." : "verify.json is missing.",
|
|
475
|
+
});
|
|
476
|
+
if (!existsSync(verifyJsonFile)) {
|
|
477
|
+
addManualVerification("verify.json is missing; determine whether independent verification was expected.");
|
|
360
478
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
479
|
+
if (existsSync(verifyJsonFile)) {
|
|
480
|
+
try {
|
|
481
|
+
const verify = JSON.parse(readFileSync(verifyJsonFile, "utf-8"));
|
|
482
|
+
checks.push({
|
|
483
|
+
name: "verify_status",
|
|
484
|
+
result: verify.status === "passed" ? "pass" : verify.status === "failed" ? "fail" : "warn",
|
|
485
|
+
detail: `Structured verification status is "${verify.status || "unknown"}".`,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
checks.push({ name: "verify_status", result: "fail", detail: "verify.json is invalid JSON." });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// Phase 4: Use new_out_of_scope_changes (task-caused) instead of out_of_scope_changes (all)
|
|
493
|
+
// Pre-existing external dirty files that didn't change during the task should NOT fail audit.
|
|
494
|
+
const newOutOfScope = Array.isArray(statusData.new_out_of_scope_changes)
|
|
495
|
+
? statusData.new_out_of_scope_changes
|
|
496
|
+
: Array.isArray(statusData.out_of_scope_changes)
|
|
497
|
+
? statusData.out_of_scope_changes
|
|
498
|
+
: [];
|
|
499
|
+
checks.push({
|
|
500
|
+
name: "scope_changes",
|
|
501
|
+
result: newOutOfScope.length > 0 ? "fail" : "pass",
|
|
502
|
+
detail: newOutOfScope.length > 0
|
|
503
|
+
? `${newOutOfScope.length} new out-of-scope change(s) detected during task execution.`
|
|
504
|
+
: "No new out-of-scope changes recorded.",
|
|
505
|
+
});
|
|
506
|
+
// Extract changed_files from change evidence (used by several v0.7.2 checks).
|
|
507
|
+
let changedFiles = [];
|
|
508
|
+
const changedFilesFile = join(taskDir, "changed-files.json");
|
|
509
|
+
if (existsSync(changedFilesFile)) {
|
|
510
|
+
try {
|
|
511
|
+
const changeEvidence = JSON.parse(readFileSync(changedFilesFile, "utf-8"));
|
|
512
|
+
if (Array.isArray(changeEvidence.changed_files)) {
|
|
513
|
+
changedFiles = changeEvidence.changed_files;
|
|
514
|
+
}
|
|
515
|
+
const hygiene = changeEvidence.artifact_hygiene;
|
|
516
|
+
if (hygiene?.counts) {
|
|
517
|
+
const trackedArtifacts = Number(hygiene.counts.tracked_build_artifacts || 0);
|
|
518
|
+
const ignoredArtifacts = Number(hygiene.counts.ignored_untracked_artifacts || 0);
|
|
519
|
+
const runtimeFiles = Number(hygiene.counts.runtime_generated_files || 0);
|
|
520
|
+
const suspicious = Number(hygiene.counts.suspicious_changes || 0);
|
|
521
|
+
checks.push({
|
|
522
|
+
name: "artifact_hygiene",
|
|
523
|
+
result: suspicious > 0 ? "warn" : "pass",
|
|
524
|
+
detail: suspicious > 0
|
|
525
|
+
? `${suspicious} generated or artifact-like change(s) are tracked or not ignored and require review.`
|
|
526
|
+
: `${ignoredArtifacts} ignored artifact change(s) and ${runtimeFiles} runtime-generated change(s) are classified separately from source risk.`,
|
|
527
|
+
});
|
|
528
|
+
if (trackedArtifacts > 0) {
|
|
529
|
+
risks.push({ severity: "medium", description: `${trackedArtifacts} tracked build artifact change(s) require intentional source-control review.` });
|
|
530
|
+
addPossibleFalsePositive("artifact_hygiene", "Tracked build outputs may be intentional release assets rather than accidental source changes.");
|
|
531
|
+
}
|
|
532
|
+
if (suspicious > 0) {
|
|
533
|
+
actions.push("Review artifact_hygiene.suspicious_changes before accepting the task; add generated paths to Git ignore rules when appropriate.");
|
|
534
|
+
addPossibleFalsePositive("artifact_hygiene", "Artifact-like path classification is heuristic and may include intentionally maintained files.");
|
|
535
|
+
addManualVerification("Review artifact_hygiene.suspicious_changes and decide whether each path is intentional.");
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
checks.push({ name: "artifact_hygiene", result: "warn", detail: "Change evidence uses the legacy format without artifact classification." });
|
|
540
|
+
addManualVerification("Legacy changed-files evidence has no artifact classification; inspect changed paths manually.");
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
checks.push({ name: "artifact_hygiene", result: "warn", detail: "changed-files.json could not be parsed for artifact classification." });
|
|
545
|
+
addManualVerification("changed-files.json could not be parsed; inspect repository changes directly.");
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// ── 3. test.log ──
|
|
549
|
+
const testLogFile = join(taskDir, "test.log");
|
|
550
|
+
const hasTestLog = existsSync(testLogFile);
|
|
551
|
+
checks.push({
|
|
552
|
+
name: "test_log_exists",
|
|
553
|
+
result: hasTestLog ? "pass" : "warn",
|
|
554
|
+
detail: hasTestLog ? "test.log found." : "test.log is missing.",
|
|
555
|
+
});
|
|
556
|
+
if (!hasTestLog)
|
|
557
|
+
addManualVerification("test.log is missing; confirm whether the task required an agent-side test run.");
|
|
558
|
+
// ── 4. git.diff ──
|
|
559
|
+
const diffFile = join(taskDir, "git.diff");
|
|
560
|
+
checks.push({
|
|
561
|
+
name: "git_diff_exists",
|
|
562
|
+
result: existsSync(diffFile) ? "pass" : "warn",
|
|
563
|
+
detail: existsSync(diffFile) ? "git.diff found." : "git.diff is missing.",
|
|
564
|
+
});
|
|
565
|
+
if (!existsSync(diffFile))
|
|
566
|
+
addManualVerification("git.diff is missing; inspect repository state before accepting code changes.");
|
|
567
|
+
// ── 5. repo_path consistency — use resolved_repo_path, NOT resolve() ──
|
|
568
|
+
let repoPathSafe = "";
|
|
569
|
+
let repoConsistent = false;
|
|
570
|
+
try {
|
|
571
|
+
// Prefer the pre-resolved absolute path from task metadata
|
|
572
|
+
const resolvedRepoPath = statusData.resolved_repo_path;
|
|
573
|
+
if (resolvedRepoPath && typeof resolvedRepoPath === "string") {
|
|
574
|
+
repoPathSafe = guardWorkspacePath(resolvedRepoPath, config.workspaceRoot);
|
|
575
|
+
}
|
|
576
|
+
else if (statusData.repo_path) {
|
|
577
|
+
repoPathSafe = guardWorkspacePath(statusData.repo_path, config.workspaceRoot);
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
repoPathSafe = resolve(config.workspaceRoot);
|
|
581
|
+
}
|
|
582
|
+
repoConsistent = true;
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
repoConsistent = false;
|
|
586
|
+
repoPathSafe = statusData.resolved_repo_path || statusData.repo_path || config.workspaceRoot;
|
|
587
|
+
}
|
|
588
|
+
checks.push({
|
|
589
|
+
name: "repo_path_consistency",
|
|
590
|
+
result: repoConsistent ? "pass" : "fail",
|
|
591
|
+
detail: repoConsistent
|
|
592
|
+
? `repo_path "${statusData.repo_path || "."}" resolves within workspace.`
|
|
593
|
+
: `repo_path "${statusData.repo_path}" is outside workspace.`,
|
|
594
|
+
});
|
|
595
|
+
if (!repoConsistent)
|
|
596
|
+
risks.push({ severity: "high", description: "repo_path inconsistent with workspace." });
|
|
597
|
+
// ── 6. package.json scripts ──
|
|
598
|
+
const pkgJsonPath = join(repoPathSafe, "package.json");
|
|
599
|
+
let pkgScripts = [];
|
|
600
|
+
if (existsSync(pkgJsonPath)) {
|
|
601
|
+
try {
|
|
602
|
+
guardSensitivePath(pkgJsonPath);
|
|
603
|
+
const pkgData = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
604
|
+
pkgScripts = Object.keys(pkgData.scripts || {});
|
|
605
|
+
checks.push({
|
|
606
|
+
name: "package_json_scripts",
|
|
607
|
+
result: "pass",
|
|
608
|
+
detail: `package.json found with ${pkgScripts.length} scripts: ${pkgScripts.join(", ") || "(none)"}.`,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
checks.push({ name: "package_json_scripts", result: "warn", detail: "package.json exists but could not be read (may be sensitive)." });
|
|
613
|
+
addManualVerification("package.json could not be read, so documented commands were not fully cross-checked.");
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
// ── 7. Scan all docs (result.md, README.md, docs/**/*.md) ──
|
|
617
|
+
const docsToScan = [];
|
|
618
|
+
if (hasResult)
|
|
619
|
+
docsToScan.push(resultFile);
|
|
620
|
+
const readmePath = join(repoPathSafe, "README.md");
|
|
621
|
+
if (existsSync(readmePath))
|
|
622
|
+
docsToScan.push(readmePath);
|
|
623
|
+
const docsDir = join(repoPathSafe, "docs");
|
|
624
|
+
if (existsSync(docsDir)) {
|
|
625
|
+
docsToScan.push(...findMdFiles(docsDir));
|
|
626
|
+
}
|
|
627
|
+
// Collect all npm run references from all docs
|
|
628
|
+
const allNpmRunRefs = new Set();
|
|
629
|
+
const allReleaseClaims = [];
|
|
630
|
+
for (const docPath of docsToScan) {
|
|
631
|
+
let content;
|
|
632
|
+
try {
|
|
633
|
+
if (docPath !== resultFile) {
|
|
634
|
+
guardReadPath(docPath, config.workspaceRoot);
|
|
635
|
+
guardSensitivePath(docPath);
|
|
636
|
+
}
|
|
637
|
+
content = readFileSync(docPath, "utf-8");
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
// Extract npm run xxx
|
|
643
|
+
const refs = content.match(/npm(?:\.cmd)?\s+run\s+([a-zA-Z0-9:_-]+)/gi) || [];
|
|
644
|
+
for (const ref of refs) {
|
|
645
|
+
const scriptName = ref.replace(/npm\s+run\s+/i, "").replace(/[^a-zA-Z0-9:_-]/g, "");
|
|
646
|
+
if (scriptName)
|
|
647
|
+
allNpmRunRefs.add(scriptName);
|
|
648
|
+
}
|
|
649
|
+
// Check release claims
|
|
650
|
+
const claims = scanForReleaseClaims(content);
|
|
651
|
+
for (const c of claims)
|
|
652
|
+
allReleaseClaims.push(`[${relative(repoPathSafe, docPath) || docPath}] ${c}`);
|
|
653
|
+
}
|
|
654
|
+
// Cross-check npm run refs against package.json scripts
|
|
655
|
+
for (const scriptName of allNpmRunRefs) {
|
|
656
|
+
if (pkgScripts.length > 0 && !pkgScripts.includes(scriptName)) {
|
|
657
|
+
checks.push({
|
|
658
|
+
name: `npm_script_${scriptName}`,
|
|
659
|
+
result: "warn",
|
|
660
|
+
detail: `Docs mention "npm run ${scriptName}" but this script is missing from package.json.`,
|
|
661
|
+
});
|
|
662
|
+
risks.push({
|
|
663
|
+
severity: "medium",
|
|
664
|
+
description: `Command "npm run ${scriptName}" referenced in docs but not found in package.json scripts.`,
|
|
665
|
+
});
|
|
666
|
+
actions.push(`Verify whether "npm run ${scriptName}" should exist or if the agent fabricated it.`);
|
|
667
|
+
addPossibleFalsePositive(`npm_script_${scriptName}`, "Documentation may describe another package, historical version, or example command rather than the current package.json.");
|
|
668
|
+
addManualVerification(`Check whether documented command "npm run ${scriptName}" belongs to another package or should be added here.`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (allNpmRunRefs.size > 0 && pkgScripts.length > 0) {
|
|
672
|
+
const missing = [...allNpmRunRefs].filter(s => !pkgScripts.includes(s));
|
|
673
|
+
if (missing.length === 0) {
|
|
674
|
+
checks.push({
|
|
675
|
+
name: "npm_scripts_crosscheck",
|
|
676
|
+
result: "pass",
|
|
677
|
+
detail: `All ${allNpmRunRefs.size} npm run references in docs exist in package.json.`,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
// ── 8. Release claims always flagged as unverified ──
|
|
682
|
+
if (allReleaseClaims.length > 0) {
|
|
683
|
+
checks.push({
|
|
684
|
+
name: "release_claims_unverified",
|
|
685
|
+
result: "warn",
|
|
686
|
+
detail: `Found ${allReleaseClaims.length} remote publish/release claim(s): ${allReleaseClaims.slice(0, 3).join("; ")}${allReleaseClaims.length > 3 ? "..." : ""}. These are UNVERIFIED.`,
|
|
687
|
+
});
|
|
688
|
+
risks.push({
|
|
689
|
+
severity: "high",
|
|
690
|
+
description: `${allReleaseClaims.length} remote publish/release/deploy claim(s) found in docs. PatchWarden cannot independently verify npm/GitHub actions. Manual confirmation required.`,
|
|
691
|
+
});
|
|
692
|
+
actions.push("Manually verify all npm publish / GitHub release / git tag claims before accepting the task as complete.");
|
|
693
|
+
addManualVerification("Verify remote npm, GitHub Release, and Git tag claims against authoritative remote services.");
|
|
694
|
+
}
|
|
695
|
+
// ── 9. test.log Exit code check ──
|
|
696
|
+
let testLogContent = null;
|
|
697
|
+
if (hasTestLog) {
|
|
698
|
+
let logContent;
|
|
699
|
+
try {
|
|
700
|
+
logContent = readFileSync(testLogFile, "utf-8");
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
logContent = "";
|
|
704
|
+
}
|
|
705
|
+
testLogContent = logContent;
|
|
706
|
+
// Check test_command visibility
|
|
707
|
+
if (statusData.test_command && logContent.includes(statusData.test_command)) {
|
|
708
|
+
checks.push({ name: "test_command_in_log", result: "pass", detail: "test.log contains the configured test command." });
|
|
709
|
+
}
|
|
710
|
+
else if (statusData.test_command) {
|
|
711
|
+
checks.push({ name: "test_command_in_log", result: "warn", detail: `test.log does not clearly show "${statusData.test_command}".` });
|
|
712
|
+
addManualVerification("Confirm that test.log belongs to the configured test command.");
|
|
713
|
+
}
|
|
714
|
+
// Extract Exit code
|
|
715
|
+
const exitMatch = logContent.match(/Exit\s*code:\s*(\d+)/i);
|
|
716
|
+
if (exitMatch) {
|
|
717
|
+
const exitCode = parseInt(exitMatch[1]);
|
|
718
|
+
if (exitCode === 0) {
|
|
719
|
+
checks.push({ name: "test_exit_code", result: "pass", detail: "Test exit code is 0." });
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
checks.push({ name: "test_exit_code", result: "fail", detail: `Test exit code is ${exitCode} (non-zero).` });
|
|
723
|
+
risks.push({ severity: "high", description: `Tests failed with exit code ${exitCode}.` });
|
|
724
|
+
actions.push("Review test.log failures and fix before accepting this task.");
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
else if (statusData.test_command) {
|
|
728
|
+
checks.push({ name: "test_exit_code", result: "warn", detail: "test.log does not contain 'Exit code:' line — cannot verify test result." });
|
|
729
|
+
risks.push({ severity: "medium", description: "Test command was configured but test.log has no exit code." });
|
|
730
|
+
addManualVerification("The configured test command has no recorded exit code; rerun or inspect verification evidence.");
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
// ── v0.7.2: New audit checks ──
|
|
734
|
+
// Read result.md content (guarded) for the unrecorded_command_execution check.
|
|
735
|
+
let resultMdContent = null;
|
|
736
|
+
if (hasResult) {
|
|
737
|
+
try {
|
|
738
|
+
resultMdContent = readFileSync(resultFile, "utf-8");
|
|
739
|
+
}
|
|
740
|
+
catch {
|
|
741
|
+
resultMdContent = null;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const verifyCommands = Array.isArray(statusData.verify_commands) ? statusData.verify_commands : [];
|
|
745
|
+
const testCommand = typeof statusData.test_command === "string" ? statusData.test_command : null;
|
|
746
|
+
const forbidden = Array.isArray(statusData.forbidden) ? statusData.forbidden : null;
|
|
747
|
+
const doneEvidence = Array.isArray(statusData.done_evidence) ? statusData.done_evidence : null;
|
|
748
|
+
const newChecks = [
|
|
749
|
+
checkForbiddenScope(changedFiles, forbidden),
|
|
750
|
+
checkDoneEvidenceMissing(taskDir, doneEvidence),
|
|
751
|
+
checkReadmeChangelogSync(changedFiles),
|
|
752
|
+
checkPackageManifestConsistency(changedFiles, repoPathSafe),
|
|
753
|
+
checkSensitivePathAccess(changedFiles),
|
|
754
|
+
checkUnrecordedCommandExecution(testLogContent, resultMdContent, verifyCommands, testCommand, repoPathSafe),
|
|
755
|
+
];
|
|
756
|
+
for (const check of newChecks) {
|
|
757
|
+
if (check)
|
|
758
|
+
checks.push(check);
|
|
759
|
+
}
|
|
760
|
+
// ── 10. Summarize ──
|
|
761
|
+
const failCount = checks.filter((c) => c.result === "fail").length;
|
|
762
|
+
const warnCount = checks.filter((c) => c.result === "warn").length;
|
|
763
|
+
const passCount = checks.filter((c) => c.result === "pass").length;
|
|
764
|
+
const verdict = failCount > 0 ? "fail" : warnCount > 0 ? "warn" : "pass";
|
|
765
|
+
const confirmedFailures = checks.filter((check) => check.result === "fail");
|
|
766
|
+
let summary = `Audit complete: ${passCount} pass, ${warnCount} warn, ${failCount} fail across ${checks.length} checks. `;
|
|
767
|
+
summary += risks.length > 0 ? `${risks.length} risk(s) identified. ` : "No risks identified. ";
|
|
768
|
+
summary += `${confirmedFailures.length} confirmed failure(s), ${possibleFalsePositives.length} possible false-positive warning(s), and ${manualVerificationItems.length} manual verification item(s).`;
|
|
769
|
+
if (actions.length === 0) {
|
|
770
|
+
actions.push("No specific actions recommended.");
|
|
771
|
+
}
|
|
772
|
+
// Write independent-review.md
|
|
773
|
+
const reviewMd = [
|
|
774
|
+
"# Independent Review",
|
|
775
|
+
"",
|
|
776
|
+
`**Task**: ${taskId}`,
|
|
777
|
+
`**Verdict**: ${verdict.toUpperCase()}`,
|
|
778
|
+
"",
|
|
779
|
+
"## Summary",
|
|
780
|
+
summary,
|
|
781
|
+
"",
|
|
782
|
+
"## Checks",
|
|
783
|
+
...checks.map((c) => `- [${c.result === "pass" ? "x" : " "}] **${c.name}**: ${c.detail}`),
|
|
784
|
+
"",
|
|
785
|
+
"## Risks",
|
|
786
|
+
...risks.map((r) => `- [${r.severity}] ${r.description}`),
|
|
787
|
+
"",
|
|
788
|
+
"## Confirmed Failures",
|
|
789
|
+
...(confirmedFailures.length > 0
|
|
790
|
+
? confirmedFailures.map((check) => `- **${check.name}**: ${check.detail}`)
|
|
791
|
+
: ["- None."]),
|
|
792
|
+
"",
|
|
793
|
+
"## Possible False Positives",
|
|
794
|
+
...(possibleFalsePositives.length > 0
|
|
795
|
+
? possibleFalsePositives.map((item) => `- **${item.check}**: ${item.reason}`)
|
|
796
|
+
: ["- None identified."]),
|
|
797
|
+
"",
|
|
798
|
+
"## Manual Verification Required",
|
|
799
|
+
...(manualVerificationItems.length > 0
|
|
800
|
+
? manualVerificationItems.map((item) => `- ${item}`)
|
|
801
|
+
: ["- No additional manual verification identified by this audit."]),
|
|
802
|
+
"",
|
|
803
|
+
"## Recommended Actions",
|
|
804
|
+
...actions.map((a) => `- ${a}`),
|
|
805
|
+
].join("\n");
|
|
806
|
+
writeFileSync(join(taskDir, "independent-review.md"), reviewMd, "utf-8");
|
|
807
|
+
// ── v0.7.2: 集成 acceptanceEngine ──
|
|
808
|
+
const releaseClaimsUnverified = allReleaseClaims.length > 0;
|
|
809
|
+
const acceptanceEvidence = {
|
|
810
|
+
task_id: taskId,
|
|
811
|
+
task_status: taskStatus,
|
|
812
|
+
result_md_exists: hasResult,
|
|
813
|
+
result_json_exists: existsSync(resultJsonFile),
|
|
814
|
+
verify_json_exists: existsSync(verifyJsonFile),
|
|
815
|
+
test_log_exists: hasTestLog,
|
|
816
|
+
git_diff_exists: existsSync(join(taskDir, "git.diff")),
|
|
817
|
+
verify_status: (() => {
|
|
818
|
+
if (!existsSync(verifyJsonFile))
|
|
819
|
+
return null;
|
|
820
|
+
try {
|
|
821
|
+
const v = JSON.parse(readFileSync(verifyJsonFile, "utf-8"));
|
|
822
|
+
return v.status === "passed" ? "passed" : v.status === "failed" ? "failed" : "skipped";
|
|
823
|
+
}
|
|
824
|
+
catch {
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
})(),
|
|
828
|
+
new_out_of_scope_changes: newOutOfScope,
|
|
829
|
+
goal: statusData.goal ?? null,
|
|
830
|
+
scope: Array.isArray(statusData.scope) ? statusData.scope : null,
|
|
831
|
+
forbidden: Array.isArray(statusData.forbidden) ? statusData.forbidden : null,
|
|
832
|
+
verification: Array.isArray(statusData.verification) ? statusData.verification : null,
|
|
833
|
+
done_evidence: Array.isArray(statusData.done_evidence) ? statusData.done_evidence : null,
|
|
834
|
+
artifact_status: statusData.artifact_status ?? null,
|
|
835
|
+
release_claims_unverified: releaseClaimsUnverified,
|
|
836
|
+
checks: checks.map((c) => ({ name: c.name, result: c.result, detail: c.detail })),
|
|
837
|
+
};
|
|
838
|
+
const acceptanceResult = evaluateAcceptance(acceptanceEvidence);
|
|
839
|
+
// 回写 status.json 的 acceptance_status(仅对 done_by_agent 有意义)
|
|
840
|
+
if (taskStatus === "done_by_agent") {
|
|
841
|
+
const updatedStatus = {
|
|
842
|
+
...statusData,
|
|
843
|
+
acceptance_status: acceptanceResult.acceptance_status,
|
|
844
|
+
updated_at: new Date().toISOString(),
|
|
845
|
+
};
|
|
846
|
+
writeFileSync(statusFile, JSON.stringify(updatedStatus, null, 2), "utf-8");
|
|
847
|
+
}
|
|
848
|
+
// 导出 ACCEPTANCE.md
|
|
849
|
+
const acceptanceMd = renderAcceptanceMarkdown(taskId, acceptanceResult, acceptanceEvidence);
|
|
850
|
+
writeFileSync(join(taskDir, "ACCEPTANCE.md"), acceptanceMd, "utf-8");
|
|
851
|
+
return {
|
|
852
|
+
task_id: taskId,
|
|
853
|
+
verdict,
|
|
854
|
+
acceptance: {
|
|
855
|
+
verdict: acceptanceResult.verdict,
|
|
856
|
+
status: acceptanceResult.acceptance_status ?? "null",
|
|
857
|
+
reason: acceptanceResult.reason,
|
|
858
|
+
reasons: acceptanceResult.reasons,
|
|
859
|
+
required_evidence: acceptanceResult.required_evidence,
|
|
860
|
+
next_suggested_task: acceptanceResult.next_suggested_task,
|
|
861
|
+
fail_checks: acceptanceResult.fail_checks,
|
|
862
|
+
warn_checks: acceptanceResult.warn_checks,
|
|
863
|
+
},
|
|
864
|
+
summary,
|
|
865
|
+
checks,
|
|
866
|
+
risks,
|
|
867
|
+
confirmed_failures: confirmedFailures,
|
|
868
|
+
possible_false_positives: possibleFalsePositives,
|
|
869
|
+
manual_verification_required: manualVerificationItems.length > 0,
|
|
870
|
+
manual_verification_items: manualVerificationItems,
|
|
871
|
+
recommended_next_actions: actions,
|
|
872
|
+
};
|
|
444
873
|
}
|