patchwarden 0.6.4 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +54 -20
- package/README.md +48 -21
- package/dist/agents/agentRouter.d.ts +40 -0
- package/dist/agents/agentRouter.js +95 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +6 -2
- package/dist/controlCenter.js +212 -3
- package/dist/direct/directSessionStore.d.ts +2 -0
- package/dist/direct/directVerification.js +7 -0
- package/dist/doctor.js +32 -1
- package/dist/goal/acceptanceEngine.d.ts +62 -0
- package/dist/goal/acceptanceEngine.js +103 -0
- package/dist/goal/acceptanceTemplate.d.ts +10 -0
- package/dist/goal/acceptanceTemplate.js +104 -0
- package/dist/goal/goalGraph.d.ts +58 -0
- package/dist/goal/goalGraph.js +189 -0
- package/dist/goal/goalProgress.d.ts +81 -0
- package/dist/goal/goalProgress.js +216 -0
- package/dist/goal/goalStatus.d.ts +60 -0
- package/dist/goal/goalStatus.js +137 -0
- package/dist/goal/goalStore.d.ts +79 -0
- package/dist/goal/goalStore.js +211 -0
- package/dist/goal/handoffExport.d.ts +34 -0
- package/dist/goal/handoffExport.js +183 -0
- package/dist/goal/subgoalSync.d.ts +40 -0
- package/dist/goal/subgoalSync.js +72 -0
- package/dist/goal/worktreeManager.d.ts +96 -0
- package/dist/goal/worktreeManager.js +292 -0
- package/dist/logging.d.ts +44 -0
- package/dist/logging.js +68 -0
- package/dist/policy/projectPolicy.d.ts +55 -0
- package/dist/policy/projectPolicy.js +286 -0
- package/dist/release/releaseGate.d.ts +99 -0
- package/dist/release/releaseGate.js +475 -0
- package/dist/runner/postTaskCleanup.d.ts +13 -0
- package/dist/runner/postTaskCleanup.js +147 -0
- package/dist/runner/runTask.js +50 -10
- package/dist/security/discoveryTokenStore.d.ts +62 -0
- package/dist/security/discoveryTokenStore.js +100 -0
- package/dist/security/toolInvocationGuard.d.ts +35 -0
- package/dist/security/toolInvocationGuard.js +153 -0
- package/dist/smoke-test.js +18 -10
- package/dist/taskRuntime.d.ts +17 -0
- package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
- package/dist/test/unit/acceptance-engine.test.js +228 -0
- package/dist/test/unit/agent-router.test.d.ts +1 -0
- package/dist/test/unit/agent-router.test.js +287 -0
- package/dist/test/unit/audit-checks.test.d.ts +1 -0
- package/dist/test/unit/audit-checks.test.js +350 -0
- package/dist/test/unit/diagnose-task.test.d.ts +1 -0
- package/dist/test/unit/diagnose-task.test.js +457 -0
- package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
- package/dist/test/unit/discovery-token-store.test.js +139 -0
- package/dist/test/unit/evidence-pack.test.d.ts +1 -0
- package/dist/test/unit/evidence-pack.test.js +130 -0
- package/dist/test/unit/goal-graph.test.d.ts +1 -0
- package/dist/test/unit/goal-graph.test.js +298 -0
- package/dist/test/unit/goal-progress.test.d.ts +1 -0
- package/dist/test/unit/goal-progress.test.js +381 -0
- package/dist/test/unit/goal-status.test.d.ts +1 -0
- package/dist/test/unit/goal-status.test.js +215 -0
- package/dist/test/unit/goal-store.test.d.ts +1 -0
- package/dist/test/unit/goal-store.test.js +253 -0
- package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
- package/dist/test/unit/goal-subgoal-task.test.js +55 -0
- package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
- package/dist/test/unit/goal-tools-registry.test.js +190 -0
- package/dist/test/unit/handoff-export.test.d.ts +1 -0
- package/dist/test/unit/handoff-export.test.js +263 -0
- package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
- package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
- package/dist/test/unit/logging.test.js +127 -5
- package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
- package/dist/test/unit/post-task-cleanup.test.js +48 -0
- package/dist/test/unit/project-policy-release-mode.test.d.ts +1 -0
- package/dist/test/unit/project-policy-release-mode.test.js +125 -0
- package/dist/test/unit/reconcile-tasks.test.d.ts +1 -0
- package/dist/test/unit/reconcile-tasks.test.js +456 -0
- package/dist/test/unit/release-gate.test.d.ts +1 -0
- package/dist/test/unit/release-gate.test.js +242 -0
- package/dist/test/unit/run-task-loop.test.d.ts +1 -0
- package/dist/test/unit/run-task-loop.test.js +380 -0
- package/dist/test/unit/safe-views.test.d.ts +1 -0
- package/dist/test/unit/safe-views.test.js +171 -0
- package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
- package/dist/test/unit/schema-drift-check.test.js +176 -0
- package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
- package/dist/test/unit/subgoal-sync.test.js +183 -0
- package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
- package/dist/test/unit/tool-invocation-guard.test.js +432 -0
- package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
- package/dist/test/unit/tool-usage-stats.test.js +300 -0
- package/dist/test/unit/toolSearch.test.d.ts +1 -0
- package/dist/test/unit/toolSearch.test.js +571 -0
- package/dist/test/unit/worktree-manager.test.d.ts +1 -0
- package/dist/test/unit/worktree-manager.test.js +176 -0
- package/dist/tools/auditTask.d.ts +103 -1
- package/dist/tools/auditTask.js +461 -8
- package/dist/tools/cancelTask.js +1 -1
- package/dist/tools/checkReleaseGate.d.ts +21 -0
- package/dist/tools/checkReleaseGate.js +22 -0
- package/dist/tools/createTask.d.ts +18 -2
- package/dist/tools/createTask.js +29 -1
- package/dist/tools/diagnoseTask.d.ts +45 -0
- package/dist/tools/diagnoseTask.js +347 -0
- package/dist/tools/discardWorktree.d.ts +24 -0
- package/dist/tools/discardWorktree.js +27 -0
- package/dist/tools/discoverTools.d.ts +11 -0
- package/dist/tools/discoverTools.js +39 -0
- package/dist/tools/evidencePack.d.ts +39 -0
- package/dist/tools/evidencePack.js +168 -0
- package/dist/tools/explainTool.d.ts +11 -0
- package/dist/tools/explainTool.js +21 -0
- package/dist/tools/getTaskSummary.js +2 -1
- package/dist/tools/goalSubgoalTask.d.ts +51 -0
- package/dist/tools/goalSubgoalTask.js +110 -0
- package/dist/tools/healthCheck.d.ts +1 -0
- package/dist/tools/healthCheck.js +5 -1
- package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
- package/dist/tools/invokeDiscoveredTool.js +112 -0
- package/dist/tools/listTasks.d.ts +3 -1
- package/dist/tools/listTasks.js +14 -1
- package/dist/tools/mergeWorktree.d.ts +24 -0
- package/dist/tools/mergeWorktree.js +27 -0
- package/dist/tools/recommendAgentForTask.d.ts +19 -0
- package/dist/tools/recommendAgentForTask.js +56 -0
- package/dist/tools/reconcileTasks.d.ts +41 -0
- package/dist/tools/reconcileTasks.js +352 -0
- package/dist/tools/registry.js +926 -2
- package/dist/tools/releaseMode.d.ts +50 -0
- package/dist/tools/releaseMode.js +370 -0
- package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
- package/dist/tools/runDirectVerificationBundle.js +64 -0
- package/dist/tools/runTaskLoop.d.ts +57 -0
- package/dist/tools/runTaskLoop.js +417 -0
- package/dist/tools/runVerification.d.ts +4 -0
- package/dist/tools/runVerification.js +4 -0
- package/dist/tools/safeStatus.d.ts +31 -1
- package/dist/tools/safeStatus.js +43 -1
- package/dist/tools/safeViews.d.ts +262 -0
- package/dist/tools/safeViews.js +252 -0
- package/dist/tools/schemaDriftCheck.d.ts +46 -0
- package/dist/tools/schemaDriftCheck.js +80 -0
- package/dist/tools/taskLineage.d.ts +91 -0
- package/dist/tools/taskLineage.js +175 -0
- package/dist/tools/toolCatalog.d.ts +4 -3
- package/dist/tools/toolCatalog.js +39 -11
- package/dist/tools/toolRegistry.d.ts +61 -0
- package/dist/tools/toolRegistry.js +834 -0
- package/dist/tools/toolSearch.d.ts +110 -0
- package/dist/tools/toolSearch.js +331 -0
- package/dist/tools/toolUsageStats.d.ts +41 -0
- package/dist/tools/toolUsageStats.js +116 -0
- package/dist/tools/waitForTask.js +1 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watcherStatus.d.ts +29 -0
- package/dist/watcherStatus.js +92 -1
- package/docs/chatgpt-usage.md +31 -0
- package/docs/control-center/README.md +9 -0
- package/package.json +3 -2
- package/scripts/checks/control-center-smoke.js +87 -0
- package/scripts/checks/control-smoke.js +2 -2
- package/scripts/checks/doctor-smoke.js +1 -0
- package/scripts/checks/lifecycle-smoke.js +13 -9
- package/scripts/checks/mcp-manifest-check.js +65 -6
- package/scripts/checks/mcp-smoke.js +60 -9
- package/scripts/checks/watcher-supervisor-smoke.js +2 -2
- package/src/agents/agentRouter.ts +149 -0
- package/src/config.ts +9 -3
- package/src/controlCenter.ts +215 -4
- package/src/direct/directSessionStore.ts +2 -0
- package/src/direct/directVerification.ts +7 -0
- package/src/doctor.ts +36 -1
- package/src/goal/acceptanceEngine.ts +160 -0
- package/src/goal/acceptanceTemplate.ts +121 -0
- package/src/goal/goalGraph.ts +225 -0
- package/src/goal/goalProgress.ts +301 -0
- package/src/goal/goalStatus.ts +234 -0
- package/src/goal/goalStore.ts +306 -0
- package/src/goal/handoffExport.ts +211 -0
- package/src/goal/subgoalSync.ts +82 -0
- package/src/goal/worktreeManager.ts +404 -0
- package/src/logging.ts +91 -0
- package/src/policy/projectPolicy.ts +344 -0
- package/src/release/releaseGate.ts +567 -0
- package/src/runner/postTaskCleanup.ts +154 -0
- package/src/runner/runTask.ts +49 -10
- package/src/security/discoveryTokenStore.ts +157 -0
- package/src/security/toolInvocationGuard.ts +251 -0
- package/src/smoke-test.ts +15 -7
- package/src/taskRuntime.ts +17 -0
- package/src/test/unit/acceptance-engine.test.ts +261 -0
- package/src/test/unit/agent-router.test.ts +342 -0
- package/src/test/unit/audit-checks.test.ts +567 -0
- package/src/test/unit/diagnose-task.test.ts +544 -0
- package/src/test/unit/discovery-token-store.test.ts +181 -0
- package/src/test/unit/evidence-pack.test.ts +142 -0
- package/src/test/unit/goal-graph.test.ts +347 -0
- package/src/test/unit/goal-progress.test.ts +538 -0
- package/src/test/unit/goal-status.test.ts +270 -0
- package/src/test/unit/goal-store.test.ts +318 -0
- package/src/test/unit/goal-subgoal-task.test.ts +72 -0
- package/src/test/unit/goal-tools-registry.test.ts +243 -0
- package/src/test/unit/handoff-export.test.ts +295 -0
- package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
- package/src/test/unit/logging.test.ts +177 -5
- package/src/test/unit/post-task-cleanup.test.ts +53 -0
- package/src/test/unit/project-policy-release-mode.test.ts +156 -0
- package/src/test/unit/reconcile-tasks.test.ts +551 -0
- package/src/test/unit/release-gate.test.ts +314 -0
- package/src/test/unit/run-task-loop.test.ts +425 -0
- package/src/test/unit/safe-views.test.ts +184 -0
- package/src/test/unit/schema-drift-check.test.ts +260 -0
- package/src/test/unit/subgoal-sync.test.ts +236 -0
- package/src/test/unit/tool-invocation-guard.test.ts +542 -0
- package/src/test/unit/tool-usage-stats.test.ts +384 -0
- package/src/test/unit/toolSearch.test.ts +756 -0
- package/src/test/unit/worktree-manager.test.ts +247 -0
- package/src/tools/auditTask.ts +831 -402
- package/src/tools/cancelTask.ts +1 -1
- package/src/tools/checkReleaseGate.ts +45 -0
- package/src/tools/createTask.ts +64 -6
- package/src/tools/diagnoseTask.ts +460 -0
- package/src/tools/discardWorktree.ts +42 -0
- package/src/tools/discoverTools.ts +51 -0
- package/src/tools/evidencePack.ts +205 -0
- package/src/tools/explainTool.ts +26 -0
- package/src/tools/getTaskSummary.ts +2 -1
- package/src/tools/goalSubgoalTask.ts +170 -0
- package/src/tools/healthCheck.ts +4 -1
- package/src/tools/invokeDiscoveredTool.ts +163 -0
- package/src/tools/listTasks.ts +16 -2
- package/src/tools/listWorkspace.ts +71 -71
- package/src/tools/mergeWorktree.ts +42 -0
- package/src/tools/recommendAgentForTask.ts +79 -0
- package/src/tools/reconcileTasks.ts +464 -0
- package/src/tools/registry.ts +1093 -73
- package/src/tools/releaseMode.ts +450 -0
- package/src/tools/runDirectVerificationBundle.ts +98 -0
- package/src/tools/runTaskLoop.ts +526 -0
- package/src/tools/runVerification.ts +8 -0
- package/src/tools/safeStatus.ts +73 -2
- package/src/tools/safeViews.ts +273 -0
- package/src/tools/schemaDriftCheck.ts +120 -0
- package/src/tools/taskLineage.ts +300 -0
- package/src/tools/toolCatalog.ts +40 -11
- package/src/tools/toolRegistry.ts +896 -0
- package/src/tools/toolSearch.ts +464 -0
- package/src/tools/toolUsageStats.ts +151 -0
- package/src/tools/waitForTask.ts +1 -0
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +97 -1
- package/ui/pages/dashboard.html +143 -2
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface ProjectPolicy {
|
|
2
|
+
allowed_commands: string[];
|
|
3
|
+
auto_cleanup: {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
patterns: string[];
|
|
6
|
+
exclude: string[];
|
|
7
|
+
};
|
|
8
|
+
high_risk_commands: string[];
|
|
9
|
+
protected_paths: string[];
|
|
10
|
+
release_mode: {
|
|
11
|
+
version_source: string;
|
|
12
|
+
required_commands: string[];
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface ProjectPolicyIssue {
|
|
16
|
+
code: string;
|
|
17
|
+
severity: "error" | "warn";
|
|
18
|
+
field: string;
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ReleaseReadinessSummary {
|
|
22
|
+
version_source: string;
|
|
23
|
+
version: string | null;
|
|
24
|
+
package_json_version: string | null;
|
|
25
|
+
package_name: string | null;
|
|
26
|
+
version_consistent: boolean | null;
|
|
27
|
+
required_commands: Array<{
|
|
28
|
+
command: string;
|
|
29
|
+
allowed: boolean;
|
|
30
|
+
reason: string | null;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
export interface ProjectPolicySummary {
|
|
34
|
+
repo_path: string;
|
|
35
|
+
resolved_repo_path: string;
|
|
36
|
+
policy_path: string;
|
|
37
|
+
exists: boolean;
|
|
38
|
+
valid: boolean;
|
|
39
|
+
effective_policy: ProjectPolicy;
|
|
40
|
+
issues: ProjectPolicyIssue[];
|
|
41
|
+
release_readiness: ReleaseReadinessSummary;
|
|
42
|
+
}
|
|
43
|
+
export declare function getProjectPolicySummary(repoPathInput: string): ProjectPolicySummary;
|
|
44
|
+
export declare function getDefaultProjectPolicy(): ProjectPolicy;
|
|
45
|
+
export declare function commandAllowedByProjectPolicy(command: string, summary: ProjectPolicySummary): {
|
|
46
|
+
allowed: boolean;
|
|
47
|
+
reason: string | null;
|
|
48
|
+
};
|
|
49
|
+
export declare function isProtectedByProjectPolicy(relPath: string, policy: ProjectPolicy): boolean;
|
|
50
|
+
export declare function resolveVersionFromPolicy(repoPath: string, policy: ProjectPolicy): string | null;
|
|
51
|
+
export declare function readPackageJson(repoPath: string): {
|
|
52
|
+
name: string | null;
|
|
53
|
+
version: string | null;
|
|
54
|
+
githubRepo: string | null;
|
|
55
|
+
};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { getConfig, } from "../config.js";
|
|
4
|
+
import { guardTestCommand } from "../security/commandGuard.js";
|
|
5
|
+
import { guardWorkspacePath } from "../security/pathGuard.js";
|
|
6
|
+
import { isSensitivePath } from "../security/sensitiveGuard.js";
|
|
7
|
+
const DEFAULT_POLICY = {
|
|
8
|
+
allowed_commands: [],
|
|
9
|
+
auto_cleanup: {
|
|
10
|
+
enabled: true,
|
|
11
|
+
patterns: ["release-artifact-manifest.json", "frontend/dist", "release_packages"],
|
|
12
|
+
exclude: [".git", ".patchwarden", "node_modules", "docs", "samples"],
|
|
13
|
+
},
|
|
14
|
+
high_risk_commands: ["npm publish", "git push", "git tag", "gh release create"],
|
|
15
|
+
protected_paths: [".env", ".env.*", ".ssh", ".npmrc", ".pypirc", "patchwarden.config.json"],
|
|
16
|
+
release_mode: {
|
|
17
|
+
version_source: "package.json",
|
|
18
|
+
required_commands: ["npm run build", "npm test"],
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
const DANGEROUS_COMMAND_RE = /\b(?:publish|push|tag|release\s+create|deploy)\b/i;
|
|
22
|
+
const DANGEROUS_PATTERN_RE = /(^|[\\/])(?:\.git|node_modules)([\\/]|$)|^\*\*$|^\/|^[A-Za-z]:[\\/]/i;
|
|
23
|
+
export function getProjectPolicySummary(repoPathInput) {
|
|
24
|
+
const config = getConfig();
|
|
25
|
+
const repoPath = guardWorkspacePath(repoPathInput, config.workspaceRoot);
|
|
26
|
+
const policyPath = join(repoPath, ".patchwarden", "project-policy.json");
|
|
27
|
+
const issues = [];
|
|
28
|
+
let rawPolicy = {};
|
|
29
|
+
let exists = false;
|
|
30
|
+
if (existsSync(policyPath)) {
|
|
31
|
+
exists = true;
|
|
32
|
+
try {
|
|
33
|
+
const raw = readFileSync(policyPath, "utf-8").replace(/^\uFEFF/, "");
|
|
34
|
+
rawPolicy = JSON.parse(raw);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
issues.push({
|
|
38
|
+
code: "policy_json_invalid",
|
|
39
|
+
severity: "error",
|
|
40
|
+
field: ".patchwarden/project-policy.json",
|
|
41
|
+
message: `Project policy is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const effective = normalizePolicy(rawPolicy, issues);
|
|
46
|
+
validateProjectPolicy(repoPath, effective, issues, config);
|
|
47
|
+
return {
|
|
48
|
+
repo_path: repoPathInput,
|
|
49
|
+
resolved_repo_path: repoPath,
|
|
50
|
+
policy_path: ".patchwarden/project-policy.json",
|
|
51
|
+
exists,
|
|
52
|
+
valid: !issues.some((issue) => issue.severity === "error"),
|
|
53
|
+
effective_policy: effective,
|
|
54
|
+
issues,
|
|
55
|
+
release_readiness: buildReleaseReadiness(repoPath, effective, config),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function getDefaultProjectPolicy() {
|
|
59
|
+
return clonePolicy(DEFAULT_POLICY);
|
|
60
|
+
}
|
|
61
|
+
export function commandAllowedByProjectPolicy(command, summary) {
|
|
62
|
+
const trimmed = command.trim();
|
|
63
|
+
if (!trimmed)
|
|
64
|
+
return { allowed: false, reason: "empty_command" };
|
|
65
|
+
if (summary.effective_policy.high_risk_commands.includes(trimmed) || DANGEROUS_COMMAND_RE.test(trimmed)) {
|
|
66
|
+
return { allowed: false, reason: "high_risk_command" };
|
|
67
|
+
}
|
|
68
|
+
if (summary.effective_policy.allowed_commands.length > 0 &&
|
|
69
|
+
!summary.effective_policy.allowed_commands.includes(trimmed)) {
|
|
70
|
+
return { allowed: false, reason: "not_in_project_policy_allowed_commands" };
|
|
71
|
+
}
|
|
72
|
+
const readiness = summary.release_readiness.required_commands.find((entry) => entry.command === trimmed);
|
|
73
|
+
if (readiness && !readiness.allowed) {
|
|
74
|
+
return { allowed: false, reason: readiness.reason || "not_allowlisted_by_patchwarden_config" };
|
|
75
|
+
}
|
|
76
|
+
return { allowed: true, reason: null };
|
|
77
|
+
}
|
|
78
|
+
export function isProtectedByProjectPolicy(relPath, policy) {
|
|
79
|
+
const normalized = normalizeRelPath(relPath);
|
|
80
|
+
if (!normalized)
|
|
81
|
+
return true;
|
|
82
|
+
if (isSensitivePath(normalized))
|
|
83
|
+
return true;
|
|
84
|
+
return policy.protected_paths.some((pattern) => pathMatchesPattern(normalized, pattern));
|
|
85
|
+
}
|
|
86
|
+
export function resolveVersionFromPolicy(repoPath, policy) {
|
|
87
|
+
const source = policy.release_mode.version_source || "package.json";
|
|
88
|
+
if (source === "package.json")
|
|
89
|
+
return readPackageJson(repoPath).version;
|
|
90
|
+
if (source === "src/version.ts") {
|
|
91
|
+
const versionFile = safeRepoFile(repoPath, "src/version.ts");
|
|
92
|
+
if (!versionFile)
|
|
93
|
+
return null;
|
|
94
|
+
const text = readFileSync(versionFile, "utf-8");
|
|
95
|
+
const match = text.match(/PATCHWARDEN_VERSION\s*=\s*["']([^"']+)["']/);
|
|
96
|
+
return match ? match[1] : null;
|
|
97
|
+
}
|
|
98
|
+
const file = safeRepoFile(repoPath, source);
|
|
99
|
+
if (!file)
|
|
100
|
+
return null;
|
|
101
|
+
const text = readFileSync(file, "utf-8").replace(/^\uFEFF/, "").trim();
|
|
102
|
+
return text.split(/\r?\n/)[0]?.trim() || null;
|
|
103
|
+
}
|
|
104
|
+
export function readPackageJson(repoPath) {
|
|
105
|
+
const packagePath = join(repoPath, "package.json");
|
|
106
|
+
if (!existsSync(packagePath))
|
|
107
|
+
return { name: null, version: null, githubRepo: null };
|
|
108
|
+
try {
|
|
109
|
+
const data = JSON.parse(readFileSync(packagePath, "utf-8").replace(/^\uFEFF/, ""));
|
|
110
|
+
return {
|
|
111
|
+
name: typeof data.name === "string" ? data.name : null,
|
|
112
|
+
version: typeof data.version === "string" ? data.version : null,
|
|
113
|
+
githubRepo: parseGithubRepo(data.repository),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return { name: null, version: null, githubRepo: null };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function normalizePolicy(raw, issues) {
|
|
121
|
+
const policy = clonePolicy(DEFAULT_POLICY);
|
|
122
|
+
const object = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
123
|
+
policy.allowed_commands = stringArrayOrDefault(object.allowed_commands, policy.allowed_commands, "allowed_commands", issues);
|
|
124
|
+
policy.high_risk_commands = stringArrayOrDefault(object.high_risk_commands, policy.high_risk_commands, "high_risk_commands", issues);
|
|
125
|
+
policy.protected_paths = stringArrayOrDefault(object.protected_paths, policy.protected_paths, "protected_paths", issues);
|
|
126
|
+
if (object.auto_cleanup && typeof object.auto_cleanup === "object" && !Array.isArray(object.auto_cleanup)) {
|
|
127
|
+
const cleanup = object.auto_cleanup;
|
|
128
|
+
if (cleanup.enabled !== undefined && typeof cleanup.enabled !== "boolean") {
|
|
129
|
+
issues.push({ code: "invalid_type", severity: "error", field: "auto_cleanup.enabled", message: "auto_cleanup.enabled must be a boolean." });
|
|
130
|
+
}
|
|
131
|
+
else if (cleanup.enabled !== undefined) {
|
|
132
|
+
policy.auto_cleanup.enabled = cleanup.enabled;
|
|
133
|
+
}
|
|
134
|
+
policy.auto_cleanup.patterns = stringArrayOrDefault(cleanup.patterns, policy.auto_cleanup.patterns, "auto_cleanup.patterns", issues);
|
|
135
|
+
policy.auto_cleanup.exclude = stringArrayOrDefault(cleanup.exclude, policy.auto_cleanup.exclude, "auto_cleanup.exclude", issues);
|
|
136
|
+
}
|
|
137
|
+
if (object.release_mode && typeof object.release_mode === "object" && !Array.isArray(object.release_mode)) {
|
|
138
|
+
const releaseMode = object.release_mode;
|
|
139
|
+
if (releaseMode.version_source !== undefined && typeof releaseMode.version_source !== "string") {
|
|
140
|
+
issues.push({ code: "invalid_type", severity: "error", field: "release_mode.version_source", message: "release_mode.version_source must be a string." });
|
|
141
|
+
}
|
|
142
|
+
else if (releaseMode.version_source) {
|
|
143
|
+
policy.release_mode.version_source = releaseMode.version_source.trim();
|
|
144
|
+
}
|
|
145
|
+
policy.release_mode.required_commands = stringArrayOrDefault(releaseMode.required_commands, policy.release_mode.required_commands, "release_mode.required_commands", issues);
|
|
146
|
+
}
|
|
147
|
+
return policy;
|
|
148
|
+
}
|
|
149
|
+
function validateProjectPolicy(repoPath, policy, issues, config) {
|
|
150
|
+
for (const [field, commands] of [
|
|
151
|
+
["allowed_commands", policy.allowed_commands],
|
|
152
|
+
["high_risk_commands", policy.high_risk_commands],
|
|
153
|
+
["release_mode.required_commands", policy.release_mode.required_commands],
|
|
154
|
+
]) {
|
|
155
|
+
for (const command of commands) {
|
|
156
|
+
if (field !== "high_risk_commands" && DANGEROUS_COMMAND_RE.test(command)) {
|
|
157
|
+
issues.push({ code: "high_risk_command", severity: "error", field, message: `Command is release/destructive risk and cannot be auto-executed: ${command}` });
|
|
158
|
+
}
|
|
159
|
+
if (field !== "high_risk_commands") {
|
|
160
|
+
try {
|
|
161
|
+
guardTestCommand(command, config, repoPath);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
issues.push({ code: "command_not_allowlisted", severity: "warn", field, message: `Command is not allowed by existing PatchWarden config: ${command}` });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const field of ["auto_cleanup.patterns", "auto_cleanup.exclude", "protected_paths"]) {
|
|
170
|
+
const values = field === "auto_cleanup.patterns"
|
|
171
|
+
? policy.auto_cleanup.patterns
|
|
172
|
+
: field === "auto_cleanup.exclude"
|
|
173
|
+
? policy.auto_cleanup.exclude
|
|
174
|
+
: policy.protected_paths;
|
|
175
|
+
for (const value of values) {
|
|
176
|
+
if (!value.trim()) {
|
|
177
|
+
issues.push({ code: "empty_path_pattern", severity: "error", field, message: "Path patterns must be non-empty." });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const blocksCriticalDirectory = field === "auto_cleanup.patterns" && DANGEROUS_PATTERN_RE.test(value);
|
|
181
|
+
if (value.includes("\0") || value.includes("..") || isAbsolute(value) || blocksCriticalDirectory) {
|
|
182
|
+
issues.push({ code: "unsafe_path_pattern", severity: "error", field, message: `Unsafe path pattern rejected: ${value}` });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const versionSource = policy.release_mode.version_source;
|
|
187
|
+
if (versionSource && versionSource !== "package.json" && versionSource !== "src/version.ts") {
|
|
188
|
+
const resolved = safeRepoFile(repoPath, versionSource);
|
|
189
|
+
if (!resolved) {
|
|
190
|
+
issues.push({ code: "version_source_invalid", severity: "error", field: "release_mode.version_source", message: `Version source is outside repo, sensitive, or missing: ${versionSource}` });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function buildReleaseReadiness(repoPath, policy, config) {
|
|
195
|
+
const packageJson = readPackageJson(repoPath);
|
|
196
|
+
let version = null;
|
|
197
|
+
try {
|
|
198
|
+
version = resolveVersionFromPolicy(repoPath, policy);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
version = null;
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
version_source: policy.release_mode.version_source,
|
|
205
|
+
version,
|
|
206
|
+
package_json_version: packageJson.version,
|
|
207
|
+
package_name: packageJson.name,
|
|
208
|
+
version_consistent: version && packageJson.version ? version === packageJson.version : null,
|
|
209
|
+
required_commands: policy.release_mode.required_commands.map((command) => {
|
|
210
|
+
try {
|
|
211
|
+
guardTestCommand(command, config, repoPath);
|
|
212
|
+
return { command, allowed: true, reason: null };
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
return { command, allowed: false, reason: err instanceof Error ? err.message : String(err) };
|
|
216
|
+
}
|
|
217
|
+
}),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function safeRepoFile(repoPath, relPath) {
|
|
221
|
+
const normalized = normalizeRelPath(relPath);
|
|
222
|
+
if (!normalized || isSensitivePath(normalized))
|
|
223
|
+
return null;
|
|
224
|
+
const candidate = resolve(repoPath, normalized);
|
|
225
|
+
const rel = relative(repoPath, candidate);
|
|
226
|
+
if (isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`))
|
|
227
|
+
return null;
|
|
228
|
+
if (!existsSync(candidate) || !statSync(candidate).isFile())
|
|
229
|
+
return null;
|
|
230
|
+
const parent = dirname(candidate);
|
|
231
|
+
const parentRel = relative(repoPath, parent);
|
|
232
|
+
if (parentRel === ".." || parentRel.startsWith(`..${sep}`))
|
|
233
|
+
return null;
|
|
234
|
+
return candidate;
|
|
235
|
+
}
|
|
236
|
+
function stringArrayOrDefault(value, fallback, field, issues) {
|
|
237
|
+
if (value === undefined)
|
|
238
|
+
return [...fallback];
|
|
239
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
240
|
+
issues.push({ code: "invalid_type", severity: "error", field, message: `${field} must be an array of strings.` });
|
|
241
|
+
return [...fallback];
|
|
242
|
+
}
|
|
243
|
+
return [...new Set(value.map((entry) => entry.trim()).filter(Boolean))];
|
|
244
|
+
}
|
|
245
|
+
function clonePolicy(policy) {
|
|
246
|
+
return {
|
|
247
|
+
allowed_commands: [...policy.allowed_commands],
|
|
248
|
+
auto_cleanup: {
|
|
249
|
+
enabled: policy.auto_cleanup.enabled,
|
|
250
|
+
patterns: [...policy.auto_cleanup.patterns],
|
|
251
|
+
exclude: [...policy.auto_cleanup.exclude],
|
|
252
|
+
},
|
|
253
|
+
high_risk_commands: [...policy.high_risk_commands],
|
|
254
|
+
protected_paths: [...policy.protected_paths],
|
|
255
|
+
release_mode: {
|
|
256
|
+
version_source: policy.release_mode.version_source,
|
|
257
|
+
required_commands: [...policy.release_mode.required_commands],
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function normalizeRelPath(value) {
|
|
262
|
+
return value.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
263
|
+
}
|
|
264
|
+
function pathMatchesPattern(relPath, pattern) {
|
|
265
|
+
const normalizedPattern = normalizeRelPath(pattern);
|
|
266
|
+
if (!normalizedPattern)
|
|
267
|
+
return false;
|
|
268
|
+
if (normalizedPattern.includes("*")) {
|
|
269
|
+
const escaped = normalizedPattern
|
|
270
|
+
.split("*")
|
|
271
|
+
.map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
|
|
272
|
+
.join("[^/]*");
|
|
273
|
+
return new RegExp(`^${escaped}$`, "i").test(relPath);
|
|
274
|
+
}
|
|
275
|
+
return relPath.toLowerCase() === normalizedPattern.toLowerCase() ||
|
|
276
|
+
relPath.toLowerCase().startsWith(`${normalizedPattern.toLowerCase()}/`);
|
|
277
|
+
}
|
|
278
|
+
function parseGithubRepo(repository) {
|
|
279
|
+
const raw = typeof repository === "string"
|
|
280
|
+
? repository
|
|
281
|
+
: repository && typeof repository === "object" && typeof repository.url === "string"
|
|
282
|
+
? repository.url
|
|
283
|
+
: "";
|
|
284
|
+
const match = raw.match(/github\.com[:/](.+?\/.+?)(?:\.git)?(?:[#?].*)?$/i);
|
|
285
|
+
return match ? match[1] : null;
|
|
286
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PatchWarden Release Gate — v1.0.0
|
|
3
|
+
*
|
|
4
|
+
* Five-stage release verification gate. Stages run in order; any failed stage
|
|
5
|
+
* blocks all subsequent stages (they become "not_checked").
|
|
6
|
+
*
|
|
7
|
+
* 1. local_ready — npm run build + npm test + npm run doctor:ci
|
|
8
|
+
* 2. packed_ready — npm pack --dry-run + forbidden/required checks
|
|
9
|
+
* 3. published_verified — npm registry confirms version exists
|
|
10
|
+
* 4. github_release_verified— GitHub Releases confirms tag exists
|
|
11
|
+
* 5. ci_verified — GitHub Actions latest run is green
|
|
12
|
+
*
|
|
13
|
+
* Remote queries use node:https GET only (never child_process curl/git).
|
|
14
|
+
* Network errors resolve to "not_checked" so transient issues don't block.
|
|
15
|
+
* GITHUB_TOKEN is used as Bearer auth but never logged or returned.
|
|
16
|
+
*/
|
|
17
|
+
export type ReleaseStage = "local_ready" | "packed_ready" | "published_verified" | "github_release_verified" | "ci_verified";
|
|
18
|
+
export type ReleaseStageStatus = "passed" | "failed" | "not_checked";
|
|
19
|
+
export interface ReleaseGateResult {
|
|
20
|
+
target_stage: ReleaseStage;
|
|
21
|
+
stages: Record<ReleaseStage, ReleaseStageStatus>;
|
|
22
|
+
blocked_reason?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface StageCheckResult {
|
|
25
|
+
status: ReleaseStageStatus;
|
|
26
|
+
reason?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface PackedCheckResult extends StageCheckResult {
|
|
29
|
+
manifestPath?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface HttpResponse {
|
|
32
|
+
statusCode: number;
|
|
33
|
+
data: any;
|
|
34
|
+
}
|
|
35
|
+
export type HttpGetFn = (url: string, headers?: Record<string, string>, timeoutMs?: number) => Promise<HttpResponse>;
|
|
36
|
+
export interface ReleaseGateOptions {
|
|
37
|
+
packageName?: string;
|
|
38
|
+
version?: string;
|
|
39
|
+
githubRepo?: string;
|
|
40
|
+
branch?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface ReleaseGateDeps {
|
|
43
|
+
checkLocalReady?: typeof checkLocalReady;
|
|
44
|
+
checkPackedReady?: typeof checkPackedReady;
|
|
45
|
+
checkPublishedVerified?: typeof checkPublishedVerified;
|
|
46
|
+
checkGitHubReleaseVerified?: typeof checkGitHubReleaseVerified;
|
|
47
|
+
checkCiVerified?: typeof checkCiVerified;
|
|
48
|
+
httpGet?: HttpGetFn;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Compute a sha256 digest of a token for safe logging.
|
|
52
|
+
* The raw token is never persisted; only this digest may appear in logs.
|
|
53
|
+
*/
|
|
54
|
+
export declare function computeTokenDigest(token: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* Perform an HTTPS GET and parse the JSON body.
|
|
57
|
+
* Network errors and timeouts throw — callers catch and convert to "not_checked".
|
|
58
|
+
*/
|
|
59
|
+
export declare function httpsGetJson(url: string, headers?: Record<string, string>, timeoutMs?: number): Promise<HttpResponse>;
|
|
60
|
+
/**
|
|
61
|
+
* Stage 1: local_ready
|
|
62
|
+
* Runs npm.cmd run build, npm.cmd test, npm.cmd run doctor:ci in repoPath.
|
|
63
|
+
* Returns "passed" only when all three exit with code 0.
|
|
64
|
+
*/
|
|
65
|
+
export declare function checkLocalReady(repoPath: string): StageCheckResult;
|
|
66
|
+
/**
|
|
67
|
+
* Stage 2: packed_ready
|
|
68
|
+
* Runs npm.cmd pack --dry-run --json, verifies forbidden files are absent and
|
|
69
|
+
* required control files are present, then writes release-artifact-manifest.json
|
|
70
|
+
* (with sha256 + size + category for every packaged file).
|
|
71
|
+
*/
|
|
72
|
+
export declare function checkPackedReady(repoPath: string): PackedCheckResult;
|
|
73
|
+
/**
|
|
74
|
+
* Stage 3: published_verified
|
|
75
|
+
* Queries the npm registry and confirms the given version exists.
|
|
76
|
+
* Network errors resolve to "not_checked"; a 404 package or missing version is "failed".
|
|
77
|
+
*/
|
|
78
|
+
export declare function checkPublishedVerified(packageName: string, version: string, httpGet?: HttpGetFn): Promise<StageCheckResult>;
|
|
79
|
+
/**
|
|
80
|
+
* Stage 4: github_release_verified
|
|
81
|
+
* Queries GitHub Releases by tag. 200 = passed, 404 = failed, network error = not_checked.
|
|
82
|
+
* Uses GITHUB_TOKEN as Bearer auth when available; the token is never logged or returned.
|
|
83
|
+
*/
|
|
84
|
+
export declare function checkGitHubReleaseVerified(repo: string, tag: string, httpGet?: HttpGetFn): Promise<StageCheckResult>;
|
|
85
|
+
/**
|
|
86
|
+
* Stage 5: ci_verified
|
|
87
|
+
* Queries GitHub Actions runs for a branch. conclusion "success" = passed,
|
|
88
|
+
* "failure" = failed, null/other = not_checked, network error = not_checked.
|
|
89
|
+
* Uses GITHUB_TOKEN as Bearer auth when available; the token is never logged or returned.
|
|
90
|
+
*/
|
|
91
|
+
export declare function checkCiVerified(repo: string, branch: string, httpGet?: HttpGetFn): Promise<StageCheckResult>;
|
|
92
|
+
/**
|
|
93
|
+
* Run release-gate stages up to and including targetStage, in order.
|
|
94
|
+
* If any stage before the target fails, all subsequent stages become "not_checked"
|
|
95
|
+
* and blocked_reason explains the first failure.
|
|
96
|
+
*
|
|
97
|
+
* The optional `deps` parameter allows injecting mock checkers (for tests).
|
|
98
|
+
*/
|
|
99
|
+
export declare function runReleaseGateCheck(repoPath: string, targetStage: ReleaseStage, options?: ReleaseGateOptions, deps?: ReleaseGateDeps): Promise<ReleaseGateResult>;
|