patchwarden 1.1.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +83 -8
- package/README.md +78 -10
- package/dist/assessments/agentAssessor.d.ts +1 -1
- package/dist/assessments/agentAssessor.js +3 -3
- package/dist/assessments/assessmentStore.d.ts +1 -1
- package/dist/assessments/assessmentStore.js +2 -2
- package/dist/assessments/confirmCli.js +5 -4
- package/dist/config.d.ts +7 -0
- package/dist/config.js +34 -0
- package/dist/control/middleware/auth.d.ts +10 -0
- package/dist/control/middleware/auth.js +8 -0
- package/dist/control/middleware/static.d.ts +3 -0
- package/dist/control/middleware/static.js +65 -0
- package/dist/control/routes/audit.d.ts +15 -0
- package/dist/control/routes/audit.js +277 -0
- package/dist/control/routes/evidence.d.ts +4 -0
- package/dist/control/routes/evidence.js +96 -0
- package/dist/control/routes/lineage.d.ts +3 -0
- package/dist/control/routes/lineage.js +71 -0
- package/dist/control/routes/policy.d.ts +3 -0
- package/dist/control/routes/policy.js +81 -0
- package/dist/control/routes/process.d.ts +5 -0
- package/dist/control/routes/process.js +200 -0
- package/dist/control/routes/sessions.d.ts +22 -0
- package/dist/control/routes/sessions.js +224 -0
- package/dist/control/routes/status.d.ts +6 -0
- package/dist/control/routes/status.js +250 -0
- package/dist/control/routes/taskActions.d.ts +21 -0
- package/dist/control/routes/taskActions.js +233 -0
- package/dist/control/routes/tasks.d.ts +20 -0
- package/dist/control/routes/tasks.js +310 -0
- package/dist/control/routes/workspace.d.ts +15 -0
- package/dist/control/routes/workspace.js +193 -0
- package/dist/control/runtime.d.ts +91 -0
- package/dist/control/runtime.js +392 -0
- package/dist/control/server.d.ts +13 -0
- package/dist/control/server.js +479 -0
- package/dist/control/shared.d.ts +35 -0
- package/dist/control/shared.js +288 -0
- package/dist/controlCenter.d.ts +6 -0
- package/dist/controlCenter.js +7 -2001
- package/dist/direct/directGuards.js +30 -8
- package/dist/direct/directSessionStore.d.ts +2 -0
- package/dist/direct/directVerification.js +7 -0
- package/dist/doctor.d.ts +18 -1
- package/dist/doctor.js +579 -348
- package/dist/goal/goalReport.d.ts +54 -0
- package/dist/goal/goalReport.js +204 -0
- package/dist/goal/goalStatus.d.ts +6 -0
- package/dist/goal/specKitImport.d.ts +63 -0
- package/dist/goal/specKitImport.js +220 -0
- package/dist/goal/subgoalSync.js +2 -1
- package/dist/httpServer.js +15 -12
- package/dist/index.js +7 -4
- package/dist/logging.d.ts +7 -1
- package/dist/logging.js +8 -0
- package/dist/policy/projectPolicy.d.ts +55 -0
- package/dist/policy/projectPolicy.js +286 -0
- package/dist/runner/changeCapture.d.ts +3 -3
- package/dist/runner/changeCapture.js +63 -39
- package/dist/runner/cli.js +7 -6
- package/dist/runner/postTaskCleanup.js +26 -2
- package/dist/runner/runTask.js +245 -221
- package/dist/runner/simpleProcess.js +4 -4
- package/dist/runner/watch.js +17 -14
- package/dist/security/contentRedaction.d.ts +6 -0
- package/dist/security/contentRedaction.js +22 -0
- package/dist/smoke-test.js +261 -255
- package/dist/test/unit/apply-patch.test.d.ts +1 -0
- package/dist/test/unit/apply-patch.test.js +225 -0
- package/dist/test/unit/create-task.test.d.ts +1 -0
- package/dist/test/unit/create-task.test.js +197 -0
- package/dist/test/unit/direct-guards.test.js +124 -9
- package/dist/test/unit/evidence-pack.test.d.ts +1 -0
- package/dist/test/unit/evidence-pack.test.js +224 -0
- package/dist/test/unit/get-task-status.test.d.ts +1 -0
- package/dist/test/unit/get-task-status.test.js +174 -0
- package/dist/test/unit/get-task-summary.test.d.ts +1 -0
- package/dist/test/unit/get-task-summary.test.js +146 -0
- package/dist/test/unit/goal-report.test.d.ts +1 -0
- package/dist/test/unit/goal-report.test.js +159 -0
- package/dist/test/unit/goal-subgoal-task.test.js +6 -6
- package/dist/test/unit/goal-tools-registry.test.js +6 -4
- package/dist/test/unit/path-guard.test.js +24 -0
- package/dist/test/unit/project-policy-release-mode.test.d.ts +1 -0
- package/dist/test/unit/project-policy-release-mode.test.js +125 -0
- package/dist/test/unit/run-task-loop.test.d.ts +1 -0
- package/dist/test/unit/run-task-loop.test.js +380 -0
- package/dist/test/unit/schema-drift-check.test.js +10 -9
- package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
- package/dist/test/unit/spec-kit-import.test.js +341 -0
- package/dist/test/unit/wait-for-task.test.d.ts +1 -0
- package/dist/test/unit/wait-for-task.test.js +144 -0
- package/dist/tools/auditTask.d.ts +8 -63
- package/dist/tools/auditTask.js +12 -8
- package/dist/tools/createDirectSession.d.ts +1 -1
- package/dist/tools/createDirectSession.js +2 -2
- package/dist/tools/createTask.d.ts +2 -2
- package/dist/tools/createTask.js +4 -4
- package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
- package/dist/tools/dispatch/coreDispatch.js +282 -0
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
- package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
- package/dist/tools/dispatch/directDispatch.d.ts +8 -0
- package/dist/tools/dispatch/directDispatch.js +115 -0
- package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
- package/dist/tools/dispatch/goalDispatch.js +91 -0
- package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
- package/dist/tools/dispatch/releaseDispatch.js +45 -0
- package/dist/tools/dispatch/types.d.ts +23 -0
- package/dist/tools/dispatch/types.js +15 -0
- package/dist/tools/evidencePack.d.ts +45 -0
- package/dist/tools/evidencePack.js +375 -0
- package/dist/tools/finalizeDirectSession.d.ts +1 -1
- package/dist/tools/finalizeDirectSession.js +3 -3
- package/dist/tools/goalSubgoalTask.d.ts +1 -1
- package/dist/tools/goalSubgoalTask.js +2 -2
- package/dist/tools/healthCheck.js +3 -3
- package/dist/tools/recommendAgentForTask.d.ts +19 -0
- package/dist/tools/recommendAgentForTask.js +56 -0
- package/dist/tools/registry.d.ts +3 -3
- package/dist/tools/registry.js +333 -402
- package/dist/tools/releaseMode.d.ts +50 -0
- package/dist/tools/releaseMode.js +370 -0
- package/dist/tools/retryTask.d.ts +2 -2
- package/dist/tools/retryTask.js +2 -2
- package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
- package/dist/tools/runDirectVerificationBundle.js +64 -0
- package/dist/tools/runTaskLoop.d.ts +57 -0
- package/dist/tools/runTaskLoop.js +417 -0
- package/dist/tools/runVerification.d.ts +4 -0
- package/dist/tools/runVerification.js +4 -0
- package/dist/tools/safeViews.d.ts +8 -2
- package/dist/tools/safeViews.js +4 -2
- package/dist/tools/taskLineage.d.ts +91 -0
- package/dist/tools/taskLineage.js +175 -0
- package/dist/tools/toolCatalog.d.ts +2 -2
- package/dist/tools/toolCatalog.js +6 -0
- package/dist/tools/toolRegistry.js +132 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/docs/CODE_WIKI.md +983 -0
- package/docs/agentseal-integration.md +150 -0
- package/docs/architecture.md +63 -0
- package/docs/assets/patchwarden-oss-demo.gif +0 -0
- package/docs/chatgpt-usage.md +31 -0
- package/docs/control-center/README.md +9 -0
- package/docs/dashboard-overview.md +86 -0
- package/docs/demo.md +8 -0
- package/docs/direct-session-workflow.md +98 -0
- package/docs/evidence-pack-schema.md +215 -0
- package/docs/execution-plan-2026-07-09.md +315 -0
- package/docs/lineage-evidence-pack-workflow.md +127 -0
- package/docs/mcp-inspector-testing.md +200 -0
- package/docs/open-source-application.md +162 -0
- package/docs/opencode-worker.md +151 -0
- package/docs/openhands-worker.md +181 -0
- package/docs/release-evidence.md +72 -0
- package/docs/roadmap-execution-and-acceptance.md +365 -0
- package/docs/spec-kit-integration.md +131 -0
- package/docs/task-safe-review-workflow.md +98 -0
- package/docs/threat-model.md +79 -0
- package/docs/user-feedback.md +40 -0
- package/docs/why-patchwarden.md +110 -0
- package/package.json +3 -3
- package/scripts/checks/control-center-smoke.js +443 -0
- package/scripts/checks/control-smoke.js +2 -2
- package/scripts/checks/lifecycle-smoke.js +29 -23
- package/scripts/checks/mcp-manifest-check.js +12 -0
- package/scripts/checks/mcp-smoke.js +33 -7
- package/scripts/checks/watcher-supervisor-smoke.js +1 -1
- package/scripts/generate-demo-gif.py +320 -0
- package/src/assessments/agentAssessor.ts +3 -3
- package/src/assessments/assessmentStore.ts +2 -2
- package/src/assessments/confirmCli.ts +5 -4
- package/src/config.ts +37 -0
- package/src/control/middleware/auth.ts +17 -0
- package/src/control/middleware/static.ts +71 -0
- package/src/control/routes/audit.ts +321 -0
- package/src/control/routes/evidence.ts +107 -0
- package/src/control/routes/lineage.ts +92 -0
- package/src/control/routes/policy.ts +81 -0
- package/src/control/routes/process.ts +204 -0
- package/src/control/routes/sessions.ts +251 -0
- package/src/control/routes/status.ts +325 -0
- package/src/control/routes/taskActions.ts +248 -0
- package/src/control/routes/tasks.ts +323 -0
- package/src/control/routes/workspace.ts +203 -0
- package/src/control/runtime.ts +472 -0
- package/src/control/server.ts +471 -0
- package/src/control/shared.ts +294 -0
- package/src/controlCenter.ts +7 -2150
- package/src/direct/directGuards.ts +28 -7
- package/src/direct/directSessionStore.ts +2 -0
- package/src/direct/directVerification.ts +7 -0
- package/src/doctor.ts +741 -481
- package/src/goal/goalReport.ts +271 -0
- package/src/goal/goalStatus.ts +6 -0
- package/src/goal/specKitImport.ts +355 -0
- package/src/goal/subgoalSync.ts +4 -2
- package/src/httpServer.ts +17 -14
- package/src/index.ts +7 -4
- package/src/logging.ts +10 -1
- package/src/policy/projectPolicy.ts +344 -0
- package/src/runner/changeCapture.ts +70 -42
- package/src/runner/cli.ts +7 -6
- package/src/runner/postTaskCleanup.ts +26 -2
- package/src/runner/runTask.ts +325 -223
- package/src/runner/simpleProcess.ts +4 -4
- package/src/runner/watch.ts +17 -14
- package/src/security/contentRedaction.ts +29 -0
- package/src/smoke-test.ts +254 -252
- package/src/test/unit/apply-patch.test.ts +293 -0
- package/src/test/unit/create-task.test.ts +255 -0
- package/src/test/unit/direct-guards.test.ts +178 -8
- package/src/test/unit/evidence-pack.test.ts +251 -0
- package/src/test/unit/get-task-status.test.ts +203 -0
- package/src/test/unit/get-task-summary.test.ts +173 -0
- package/src/test/unit/goal-report.test.ts +189 -0
- package/src/test/unit/goal-subgoal-task.test.ts +6 -6
- package/src/test/unit/goal-tools-registry.test.ts +7 -5
- package/src/test/unit/path-guard.test.ts +30 -0
- package/src/test/unit/project-policy-release-mode.test.ts +156 -0
- package/src/test/unit/run-task-loop.test.ts +425 -0
- package/src/test/unit/schema-drift-check.test.ts +11 -9
- package/src/test/unit/spec-kit-import.test.ts +429 -0
- package/src/test/unit/wait-for-task.test.ts +176 -0
- package/src/tools/auditTask.ts +99 -59
- package/src/tools/createDirectSession.ts +3 -3
- package/src/tools/createTask.ts +7 -7
- package/src/tools/dispatch/coreDispatch.ts +374 -0
- package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
- package/src/tools/dispatch/directDispatch.ts +167 -0
- package/src/tools/dispatch/goalDispatch.ts +127 -0
- package/src/tools/dispatch/releaseDispatch.ts +65 -0
- package/src/tools/dispatch/types.ts +24 -0
- package/src/tools/evidencePack.ts +490 -0
- package/src/tools/finalizeDirectSession.ts +4 -4
- package/src/tools/goalSubgoalTask.ts +2 -2
- package/src/tools/healthCheck.ts +3 -3
- package/src/tools/listWorkspace.ts +71 -71
- package/src/tools/recommendAgentForTask.ts +79 -0
- package/src/tools/registry.ts +352 -509
- package/src/tools/releaseMode.ts +450 -0
- package/src/tools/retryTask.ts +2 -2
- package/src/tools/runDirectVerificationBundle.ts +98 -0
- package/src/tools/runTaskLoop.ts +526 -0
- package/src/tools/runVerification.ts +8 -0
- package/src/tools/safeViews.ts +4 -2
- package/src/tools/taskLineage.ts +300 -0
- package/src/tools/toolCatalog.ts +6 -0
- package/src/tools/toolRegistry.ts +132 -0
- package/src/version.ts +2 -2
- package/ui/pages/audit.html +192 -3
- package/ui/pages/dashboard.html +1070 -14
- package/ui/pages/direct-sessions.html +505 -53
- package/ui/pages/task-detail.html +456 -438
- package/ui/pages/tasks.html +598 -61
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { createGoal, readGoalStatus } from "../../goal/goalStore.js";
|
|
7
|
+
import { importSpecKitTasks, parseSpecKitJson, } from "../../goal/specKitImport.js";
|
|
8
|
+
import { PatchWardenError } from "../../errors.js";
|
|
9
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
10
|
+
let tempDir;
|
|
11
|
+
let goalId;
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-speckit-"));
|
|
14
|
+
const result = createGoal("repo", "Spec Kit Test Goal", "desc", tempDir);
|
|
15
|
+
goalId = result.goal_id;
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
function makeStandardInput() {
|
|
21
|
+
return {
|
|
22
|
+
spec: "feature-auth",
|
|
23
|
+
tasks: [
|
|
24
|
+
{
|
|
25
|
+
id: "T1",
|
|
26
|
+
desc: "Set up database schema",
|
|
27
|
+
files: ["src/db/schema.ts"],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "T2",
|
|
31
|
+
desc: "Implement auth service",
|
|
32
|
+
files: ["src/auth/service.ts", "src/auth/types.ts"],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "T3",
|
|
36
|
+
desc: "Write integration tests",
|
|
37
|
+
files: ["test/auth.test.ts"],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// ── Tests ─────────────────────────────────────────────────────────
|
|
43
|
+
describe("specKitImport", () => {
|
|
44
|
+
describe("parseSpecKitJson", () => {
|
|
45
|
+
it("解析合法 JSON 返回 SpecKitInput", () => {
|
|
46
|
+
const json = JSON.stringify({
|
|
47
|
+
spec: "my-spec",
|
|
48
|
+
tasks: [
|
|
49
|
+
{ id: "T1", desc: "Task one", files: ["a.ts"] },
|
|
50
|
+
{ id: "T2", desc: "Task two", depends_on: ["T1"] },
|
|
51
|
+
],
|
|
52
|
+
acceptance: ["T1 passes", "T2 passes"],
|
|
53
|
+
});
|
|
54
|
+
const input = parseSpecKitJson(json);
|
|
55
|
+
assert.equal(input.spec, "my-spec");
|
|
56
|
+
assert.equal(input.tasks.length, 2);
|
|
57
|
+
assert.equal(input.tasks[0].id, "T1");
|
|
58
|
+
assert.equal(input.tasks[0].desc, "Task one");
|
|
59
|
+
assert.deepEqual(input.tasks[0].files, ["a.ts"]);
|
|
60
|
+
assert.deepEqual(input.tasks[1].depends_on, ["T1"]);
|
|
61
|
+
assert.deepEqual(input.acceptance, ["T1 passes", "T2 passes"]);
|
|
62
|
+
});
|
|
63
|
+
it("缺 spec 字段抛出错误", () => {
|
|
64
|
+
const json = JSON.stringify({ tasks: [] });
|
|
65
|
+
assert.throws(() => parseSpecKitJson(json), (err) => {
|
|
66
|
+
assert.ok(err instanceof PatchWardenError);
|
|
67
|
+
assert.equal(err.reason, "invalid_spec_kit_input");
|
|
68
|
+
return true;
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
it("spec 为空字符串抛出错误", () => {
|
|
72
|
+
const json = JSON.stringify({ spec: "", tasks: [] });
|
|
73
|
+
assert.throws(() => parseSpecKitJson(json), (err) => {
|
|
74
|
+
assert.ok(err instanceof PatchWardenError);
|
|
75
|
+
assert.equal(err.reason, "invalid_spec_kit_input");
|
|
76
|
+
return true;
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
it("缺 tasks 字段抛出错误", () => {
|
|
80
|
+
const json = JSON.stringify({ spec: "my-spec" });
|
|
81
|
+
assert.throws(() => parseSpecKitJson(json), (err) => {
|
|
82
|
+
assert.ok(err instanceof PatchWardenError);
|
|
83
|
+
assert.equal(err.reason, "invalid_spec_kit_input");
|
|
84
|
+
return true;
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
it("tasks 不是数组抛出错误", () => {
|
|
88
|
+
const json = JSON.stringify({ spec: "my-spec", tasks: "not-an-array" });
|
|
89
|
+
assert.throws(() => parseSpecKitJson(json), (err) => {
|
|
90
|
+
assert.ok(err instanceof PatchWardenError);
|
|
91
|
+
assert.equal(err.reason, "invalid_spec_kit_input");
|
|
92
|
+
return true;
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
it("task 缺 id 抛出错误", () => {
|
|
96
|
+
const json = JSON.stringify({
|
|
97
|
+
spec: "my-spec",
|
|
98
|
+
tasks: [{ desc: "no id" }],
|
|
99
|
+
});
|
|
100
|
+
assert.throws(() => parseSpecKitJson(json), (err) => {
|
|
101
|
+
assert.ok(err instanceof PatchWardenError);
|
|
102
|
+
assert.equal(err.reason, "invalid_spec_kit_input");
|
|
103
|
+
return true;
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
it("无效 JSON 文本抛出错误", () => {
|
|
107
|
+
assert.throws(() => parseSpecKitJson("{ not valid json"), (err) => {
|
|
108
|
+
assert.ok(err instanceof PatchWardenError);
|
|
109
|
+
assert.equal(err.reason, "invalid_spec_kit_json");
|
|
110
|
+
return true;
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe("importSpecKitTasks — 标准导入", () => {
|
|
115
|
+
it("3 个无依赖 task → created_count=3, existing_count=0", () => {
|
|
116
|
+
const input = makeStandardInput();
|
|
117
|
+
const result = importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
|
|
118
|
+
assert.equal(result.created_count, 3);
|
|
119
|
+
assert.equal(result.existing_count, 0);
|
|
120
|
+
assert.equal(result.subgoal_ids.length, 3);
|
|
121
|
+
assert.equal(result.spec_name, "feature-auth");
|
|
122
|
+
assert.equal(result.goal_id, goalId);
|
|
123
|
+
});
|
|
124
|
+
it("每个 subgoal 的 title = task.desc", () => {
|
|
125
|
+
const input = makeStandardInput();
|
|
126
|
+
const result = importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
|
|
127
|
+
const status = readGoalStatus(goalId, tempDir);
|
|
128
|
+
assert.equal(status.subgoals.length, 3);
|
|
129
|
+
assert.equal(status.subgoals[0].title, "Set up database schema");
|
|
130
|
+
assert.equal(status.subgoals[1].title, "Implement auth service");
|
|
131
|
+
assert.equal(status.subgoals[2].title, "Write integration tests");
|
|
132
|
+
});
|
|
133
|
+
it("每个 subgoal 的 external_ref = task.id", () => {
|
|
134
|
+
const input = makeStandardInput();
|
|
135
|
+
importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
|
|
136
|
+
const status = readGoalStatus(goalId, tempDir);
|
|
137
|
+
assert.equal(status.subgoals[0].external_ref, "T1");
|
|
138
|
+
assert.equal(status.subgoals[1].external_ref, "T2");
|
|
139
|
+
assert.equal(status.subgoals[2].external_ref, "T3");
|
|
140
|
+
});
|
|
141
|
+
it("task.files 映射到 scope_hints", () => {
|
|
142
|
+
const input = makeStandardInput();
|
|
143
|
+
importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
|
|
144
|
+
const status = readGoalStatus(goalId, tempDir);
|
|
145
|
+
assert.deepEqual(status.subgoals[0].scope_hints, ["src/db/schema.ts"]);
|
|
146
|
+
assert.deepEqual(status.subgoals[1].scope_hints, [
|
|
147
|
+
"src/auth/service.ts",
|
|
148
|
+
"src/auth/types.ts",
|
|
149
|
+
]);
|
|
150
|
+
assert.deepEqual(status.subgoals[2].scope_hints, ["test/auth.test.ts"]);
|
|
151
|
+
});
|
|
152
|
+
it("subgoal_ids 对应新创建的 subgoal id", () => {
|
|
153
|
+
const input = makeStandardInput();
|
|
154
|
+
const result = importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
|
|
155
|
+
assert.deepEqual(result.subgoal_ids, ["subgoal-001", "subgoal-002", "subgoal-003"]);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
describe("importSpecKitTasks — 依赖关系导入", () => {
|
|
159
|
+
it("T2 depends_on T1 → T2 的 depends_on 含 T1 的 subgoal_id(非 external_ref)", () => {
|
|
160
|
+
const input = {
|
|
161
|
+
spec: "feature-deps",
|
|
162
|
+
tasks: [
|
|
163
|
+
{ id: "T1", desc: "Foundation task" },
|
|
164
|
+
{ id: "T2", desc: "Dependent task", depends_on: ["T1"] },
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
const result = importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
|
|
168
|
+
assert.equal(result.created_count, 2);
|
|
169
|
+
const status = readGoalStatus(goalId, tempDir);
|
|
170
|
+
assert.equal(status.subgoals.length, 2);
|
|
171
|
+
const t1Subgoal = status.subgoals[0];
|
|
172
|
+
const t2Subgoal = status.subgoals[1];
|
|
173
|
+
assert.equal(t1Subgoal.external_ref, "T1");
|
|
174
|
+
assert.equal(t2Subgoal.external_ref, "T2");
|
|
175
|
+
// T2 depends_on 应包含 T1 的 subgoal_id,而非 external_ref
|
|
176
|
+
assert.ok(t2Subgoal.depends_on.includes(t1Subgoal.id));
|
|
177
|
+
assert.ok(!t2Subgoal.depends_on.includes("T1"));
|
|
178
|
+
});
|
|
179
|
+
it("多级依赖 T3 → T2 → T1 全部正确解析", () => {
|
|
180
|
+
const input = {
|
|
181
|
+
spec: "multi-level",
|
|
182
|
+
tasks: [
|
|
183
|
+
{ id: "T1", desc: "Level 1" },
|
|
184
|
+
{ id: "T2", desc: "Level 2", depends_on: ["T1"] },
|
|
185
|
+
{ id: "T3", desc: "Level 3", depends_on: ["T2"] },
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
|
|
189
|
+
const status = readGoalStatus(goalId, tempDir);
|
|
190
|
+
assert.equal(status.subgoals.length, 3);
|
|
191
|
+
assert.deepEqual(status.subgoals[0].depends_on, []);
|
|
192
|
+
assert.deepEqual(status.subgoals[1].depends_on, ["subgoal-001"]);
|
|
193
|
+
assert.deepEqual(status.subgoals[2].depends_on, ["subgoal-002"]);
|
|
194
|
+
});
|
|
195
|
+
it("依赖不存在的 task → 忽略该依赖并继续创建", () => {
|
|
196
|
+
const input = {
|
|
197
|
+
spec: "missing-dep",
|
|
198
|
+
tasks: [
|
|
199
|
+
{ id: "T1", desc: "Task with missing dep", depends_on: ["NONEXISTENT"] },
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
const result = importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
|
|
203
|
+
assert.equal(result.created_count, 1);
|
|
204
|
+
const status = readGoalStatus(goalId, tempDir);
|
|
205
|
+
assert.equal(status.subgoals.length, 1);
|
|
206
|
+
assert.deepEqual(status.subgoals[0].depends_on, []);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
describe("importSpecKitTasks — 验收标准映射", () => {
|
|
210
|
+
it("input.acceptance 写入 GoalStatus.acceptance_criteria", () => {
|
|
211
|
+
const input = {
|
|
212
|
+
spec: "feature-acceptance",
|
|
213
|
+
tasks: [{ id: "T1", desc: "Task one" }],
|
|
214
|
+
acceptance: ["T1 passes", "T2 passes"],
|
|
215
|
+
};
|
|
216
|
+
importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
|
|
217
|
+
const status = readGoalStatus(goalId, tempDir);
|
|
218
|
+
assert.deepEqual(status.acceptance_criteria, ["T1 passes", "T2 passes"]);
|
|
219
|
+
});
|
|
220
|
+
it("无 acceptance → GoalStatus.acceptance_criteria 不存在", () => {
|
|
221
|
+
const input = {
|
|
222
|
+
spec: "no-acceptance",
|
|
223
|
+
tasks: [{ id: "T1", desc: "Task one" }],
|
|
224
|
+
};
|
|
225
|
+
importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
|
|
226
|
+
const status = readGoalStatus(goalId, tempDir);
|
|
227
|
+
assert.equal(status.acceptance_criteria, undefined);
|
|
228
|
+
});
|
|
229
|
+
it("空 acceptance 数组 → GoalStatus.acceptance_criteria 不存在", () => {
|
|
230
|
+
const input = {
|
|
231
|
+
spec: "empty-acceptance",
|
|
232
|
+
tasks: [{ id: "T1", desc: "Task one" }],
|
|
233
|
+
acceptance: [],
|
|
234
|
+
};
|
|
235
|
+
importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
|
|
236
|
+
const status = readGoalStatus(goalId, tempDir);
|
|
237
|
+
assert.equal(status.acceptance_criteria, undefined);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
describe("importSpecKitTasks — 幂等去重", () => {
|
|
241
|
+
it("重复导入相同 JSON → 第二次 created_count=0, existing_count=3", () => {
|
|
242
|
+
const input = makeStandardInput();
|
|
243
|
+
// 第一次导入
|
|
244
|
+
const result1 = importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
|
|
245
|
+
assert.equal(result1.created_count, 3);
|
|
246
|
+
assert.equal(result1.existing_count, 0);
|
|
247
|
+
// 第二次导入相同 JSON
|
|
248
|
+
const result2 = importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
|
|
249
|
+
assert.equal(result2.created_count, 0);
|
|
250
|
+
assert.equal(result2.existing_count, 3);
|
|
251
|
+
assert.deepEqual(result2.subgoal_ids, []);
|
|
252
|
+
});
|
|
253
|
+
it("重复导入后 subgoal 总数仍为 3", () => {
|
|
254
|
+
const input = makeStandardInput();
|
|
255
|
+
importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
|
|
256
|
+
importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
|
|
257
|
+
const status = readGoalStatus(goalId, tempDir);
|
|
258
|
+
assert.equal(status.subgoals.length, 3);
|
|
259
|
+
});
|
|
260
|
+
it("部分新 task 导入:只创建新增的", () => {
|
|
261
|
+
// 第一次导入 T1, T2
|
|
262
|
+
const input1 = {
|
|
263
|
+
spec: "partial",
|
|
264
|
+
tasks: [
|
|
265
|
+
{ id: "T1", desc: "Task one" },
|
|
266
|
+
{ id: "T2", desc: "Task two" },
|
|
267
|
+
],
|
|
268
|
+
};
|
|
269
|
+
const result1 = importSpecKitTasks(goalId, input1, { workspaceRoot: tempDir });
|
|
270
|
+
assert.equal(result1.created_count, 2);
|
|
271
|
+
// 第二次导入 T1, T2, T3(T1/T2 已存在)
|
|
272
|
+
const input2 = {
|
|
273
|
+
spec: "partial",
|
|
274
|
+
tasks: [
|
|
275
|
+
{ id: "T1", desc: "Task one" },
|
|
276
|
+
{ id: "T2", desc: "Task two" },
|
|
277
|
+
{ id: "T3", desc: "Task three" },
|
|
278
|
+
],
|
|
279
|
+
};
|
|
280
|
+
const result2 = importSpecKitTasks(goalId, input2, { workspaceRoot: tempDir });
|
|
281
|
+
assert.equal(result2.created_count, 1);
|
|
282
|
+
assert.equal(result2.existing_count, 2);
|
|
283
|
+
const status = readGoalStatus(goalId, tempDir);
|
|
284
|
+
assert.equal(status.subgoals.length, 3);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
describe("importSpecKitTasks — 无效输入", () => {
|
|
288
|
+
it("spec 为空 → 抛出错误且不创建 subgoal", () => {
|
|
289
|
+
const input = { spec: "", tasks: [] };
|
|
290
|
+
assert.throws(() => importSpecKitTasks(goalId, input, { workspaceRoot: tempDir }), (err) => {
|
|
291
|
+
assert.ok(err instanceof PatchWardenError);
|
|
292
|
+
assert.equal(err.reason, "invalid_spec_kit_input");
|
|
293
|
+
return true;
|
|
294
|
+
});
|
|
295
|
+
const status = readGoalStatus(goalId, tempDir);
|
|
296
|
+
assert.equal(status.subgoals.length, 0);
|
|
297
|
+
});
|
|
298
|
+
it("tasks 不是数组 → 抛出错误且不创建 subgoal", () => {
|
|
299
|
+
const input = { spec: "test", tasks: "not-array" };
|
|
300
|
+
assert.throws(() => importSpecKitTasks(goalId, input, { workspaceRoot: tempDir }), (err) => {
|
|
301
|
+
assert.ok(err instanceof PatchWardenError);
|
|
302
|
+
assert.equal(err.reason, "invalid_spec_kit_input");
|
|
303
|
+
return true;
|
|
304
|
+
});
|
|
305
|
+
const status = readGoalStatus(goalId, tempDir);
|
|
306
|
+
assert.equal(status.subgoals.length, 0);
|
|
307
|
+
});
|
|
308
|
+
it("tasks 缺失 → 抛出错误且不创建 subgoal", () => {
|
|
309
|
+
const input = { spec: "test" };
|
|
310
|
+
assert.throws(() => importSpecKitTasks(goalId, input, { workspaceRoot: tempDir }), (err) => {
|
|
311
|
+
assert.ok(err instanceof PatchWardenError);
|
|
312
|
+
assert.equal(err.reason, "invalid_spec_kit_input");
|
|
313
|
+
return true;
|
|
314
|
+
});
|
|
315
|
+
const status = readGoalStatus(goalId, tempDir);
|
|
316
|
+
assert.equal(status.subgoals.length, 0);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
describe("importSpecKitTasks — JSON 文本端到端", () => {
|
|
320
|
+
it("parseSpecKitJson + importSpecKitTasks 完整流程", () => {
|
|
321
|
+
const json = JSON.stringify({
|
|
322
|
+
spec: "e2e-spec",
|
|
323
|
+
tasks: [
|
|
324
|
+
{ id: "T1", desc: "First task", files: ["a.ts"] },
|
|
325
|
+
{ id: "T2", desc: "Second task", depends_on: ["T1"] },
|
|
326
|
+
],
|
|
327
|
+
acceptance: ["All tests pass"],
|
|
328
|
+
});
|
|
329
|
+
const input = parseSpecKitJson(json);
|
|
330
|
+
const result = importSpecKitTasks(goalId, input, { workspaceRoot: tempDir });
|
|
331
|
+
assert.equal(result.created_count, 2);
|
|
332
|
+
assert.equal(result.spec_name, "e2e-spec");
|
|
333
|
+
const status = readGoalStatus(goalId, tempDir);
|
|
334
|
+
assert.equal(status.subgoals.length, 2);
|
|
335
|
+
assert.equal(status.subgoals[0].external_ref, "T1");
|
|
336
|
+
assert.equal(status.subgoals[1].external_ref, "T2");
|
|
337
|
+
assert.deepEqual(status.subgoals[1].depends_on, ["subgoal-001"]);
|
|
338
|
+
assert.deepEqual(status.acceptance_criteria, ["All tests pass"]);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import { strict as assert } from "node:assert";
|
|
3
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { reloadConfig } from "../../config.js";
|
|
7
|
+
import { waitForTask } from "../../tools/waitForTask.js";
|
|
8
|
+
let tempDir;
|
|
9
|
+
let tasksDir;
|
|
10
|
+
let prevConfigEnv;
|
|
11
|
+
function writeConfig() {
|
|
12
|
+
const configPath = join(tempDir, "patchwarden.config.json");
|
|
13
|
+
writeFileSync(configPath, JSON.stringify({
|
|
14
|
+
workspaceRoot: tempDir,
|
|
15
|
+
tasksDir: ".patchwarden/tasks",
|
|
16
|
+
plansDir: ".patchwarden/plans",
|
|
17
|
+
assessmentsDir: ".patchwarden/assessments",
|
|
18
|
+
directSessionsDir: ".patchwarden/direct-sessions",
|
|
19
|
+
agents: { codex: { command: "codex", args: ["exec", "{prompt}"] } },
|
|
20
|
+
allowedTestCommands: ["npm test"],
|
|
21
|
+
}), "utf-8");
|
|
22
|
+
prevConfigEnv = process.env.PATCHWARDEN_CONFIG;
|
|
23
|
+
process.env.PATCHWARDEN_CONFIG = configPath;
|
|
24
|
+
reloadConfig();
|
|
25
|
+
}
|
|
26
|
+
function writeTaskStatus(taskId, status) {
|
|
27
|
+
const taskDir = join(tasksDir, taskId);
|
|
28
|
+
mkdirSync(taskDir, { recursive: true });
|
|
29
|
+
writeFileSync(join(taskDir, "status.json"), JSON.stringify(status, null, 2), "utf-8");
|
|
30
|
+
return taskDir;
|
|
31
|
+
}
|
|
32
|
+
function baseStatus(taskId, overrides = {}) {
|
|
33
|
+
return {
|
|
34
|
+
task_id: taskId,
|
|
35
|
+
plan_id: "plan-001",
|
|
36
|
+
agent: "codex",
|
|
37
|
+
workspace_root: tempDir,
|
|
38
|
+
repo_path: "repo",
|
|
39
|
+
resolved_repo_path: join(tempDir, "repo"),
|
|
40
|
+
created_at: "2026-07-12T10:00:00Z",
|
|
41
|
+
started_at: "2026-07-12T10:00:05Z",
|
|
42
|
+
finished_at: "2026-07-12T10:05:00Z",
|
|
43
|
+
updated_at: "2026-07-12T10:05:00Z",
|
|
44
|
+
timeout_seconds: 900,
|
|
45
|
+
error: null,
|
|
46
|
+
...overrides,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
describe("waitForTask", () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
tempDir = mkdtempSync(join(tmpdir(), "pw-waitfortask-"));
|
|
52
|
+
tasksDir = join(tempDir, ".patchwarden", "tasks");
|
|
53
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
54
|
+
writeConfig();
|
|
55
|
+
});
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
if (prevConfigEnv === undefined)
|
|
58
|
+
delete process.env.PATCHWARDEN_CONFIG;
|
|
59
|
+
else
|
|
60
|
+
process.env.PATCHWARDEN_CONFIG = prevConfigEnv;
|
|
61
|
+
reloadConfig();
|
|
62
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
63
|
+
});
|
|
64
|
+
// ── already terminal: done ──
|
|
65
|
+
it("returns immediately when task is already done", async () => {
|
|
66
|
+
writeTaskStatus("task-done-001", baseStatus("task-done-001", {
|
|
67
|
+
status: "done",
|
|
68
|
+
phase: "completed",
|
|
69
|
+
verify_status: "passed",
|
|
70
|
+
}));
|
|
71
|
+
const result = await waitForTask("task-done-001", 5);
|
|
72
|
+
assert.equal(result.task_id, "task-done-001");
|
|
73
|
+
assert.equal(result.status, "done");
|
|
74
|
+
assert.equal(result.terminal, true);
|
|
75
|
+
assert.equal(result.timed_out, false);
|
|
76
|
+
assert.equal(result.continuation_required, false);
|
|
77
|
+
// terminal → includes compact summary, next tool is audit_task
|
|
78
|
+
assert.ok(result.summary);
|
|
79
|
+
assert.equal(result.next_tool_call.name, "audit_task");
|
|
80
|
+
assert.equal(result.progress_summary, undefined);
|
|
81
|
+
});
|
|
82
|
+
// ── already terminal: failed ──
|
|
83
|
+
it("returns immediately when task is already failed", async () => {
|
|
84
|
+
writeTaskStatus("task-failed-001", baseStatus("task-failed-001", {
|
|
85
|
+
status: "failed",
|
|
86
|
+
phase: "failed",
|
|
87
|
+
error: "Agent exited with code 1",
|
|
88
|
+
}));
|
|
89
|
+
const result = await waitForTask("task-failed-001", 5);
|
|
90
|
+
assert.equal(result.terminal, true);
|
|
91
|
+
assert.equal(result.status, "failed");
|
|
92
|
+
assert.equal(result.timed_out, false);
|
|
93
|
+
assert.equal(result.continuation_required, false);
|
|
94
|
+
assert.ok(result.summary);
|
|
95
|
+
assert.equal(result.next_tool_call.name, "audit_task");
|
|
96
|
+
});
|
|
97
|
+
// ── running + timeout → continuation_required ──
|
|
98
|
+
it("returns continuation_required when running task times out", async () => {
|
|
99
|
+
writeTaskStatus("task-running-001", baseStatus("task-running-001", {
|
|
100
|
+
status: "running",
|
|
101
|
+
phase: "executing_agent",
|
|
102
|
+
finished_at: undefined,
|
|
103
|
+
}));
|
|
104
|
+
const result = await waitForTask("task-running-001", 1);
|
|
105
|
+
assert.equal(result.task_id, "task-running-001");
|
|
106
|
+
assert.equal(result.status, "running");
|
|
107
|
+
assert.equal(result.terminal, false);
|
|
108
|
+
assert.equal(result.timed_out, true);
|
|
109
|
+
assert.equal(result.continuation_required, true);
|
|
110
|
+
// non-terminal → includes progress_summary, next tool is wait_for_task
|
|
111
|
+
assert.ok(result.progress_summary);
|
|
112
|
+
assert.equal(result.next_tool_call.name, "wait_for_task");
|
|
113
|
+
assert.equal(result.summary, undefined);
|
|
114
|
+
// waited approximately 1 second
|
|
115
|
+
assert.ok(result.waited_ms >= 900 && result.waited_ms <= 3000);
|
|
116
|
+
});
|
|
117
|
+
// ── canceled (terminal) ──
|
|
118
|
+
it("returns terminal result for canceled task", async () => {
|
|
119
|
+
writeTaskStatus("task-canceled-001", baseStatus("task-canceled-001", {
|
|
120
|
+
status: "canceled",
|
|
121
|
+
phase: "canceled",
|
|
122
|
+
}));
|
|
123
|
+
const result = await waitForTask("task-canceled-001", 5);
|
|
124
|
+
assert.equal(result.terminal, true);
|
|
125
|
+
assert.equal(result.status, "canceled");
|
|
126
|
+
assert.equal(result.timed_out, false);
|
|
127
|
+
assert.equal(result.continuation_required, false);
|
|
128
|
+
assert.ok(result.summary);
|
|
129
|
+
assert.equal(result.next_tool_call.name, "audit_task");
|
|
130
|
+
});
|
|
131
|
+
// ── task not found ──
|
|
132
|
+
it("throws when task does not exist", async () => {
|
|
133
|
+
await assert.rejects(() => waitForTask("non-existent-task", 1), /(File not found|Task not found)/);
|
|
134
|
+
});
|
|
135
|
+
// ── invalid wait_seconds ──
|
|
136
|
+
it("throws when wait_seconds is out of range", async () => {
|
|
137
|
+
writeTaskStatus("task-validate-001", baseStatus("task-validate-001", {
|
|
138
|
+
status: "done",
|
|
139
|
+
phase: "completed",
|
|
140
|
+
}));
|
|
141
|
+
await assert.rejects(() => waitForTask("task-validate-001", 0), /wait_seconds must be an integer from 1 to 30/);
|
|
142
|
+
await assert.rejects(() => waitForTask("task-validate-001", 31), /wait_seconds must be an integer from 1 to 30/);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -32,97 +32,42 @@ export interface AuditTaskOutput {
|
|
|
32
32
|
manual_verification_items: string[];
|
|
33
33
|
recommended_next_actions: string[];
|
|
34
34
|
}
|
|
35
|
+
import type { ChangedFile } from "../runner/changeCapture.js";
|
|
35
36
|
/**
|
|
36
37
|
* Check 1: forbidden_scope_violation (fail level).
|
|
37
38
|
* Returns null when no forbidden patterns are configured (skip).
|
|
38
39
|
* Returns pass when changed files do not match any forbidden pattern.
|
|
39
40
|
* Returns fail when any changed file path (or old_path) matches a forbidden glob.
|
|
40
41
|
*/
|
|
41
|
-
export declare function checkForbiddenScope(changedFiles:
|
|
42
|
-
name: string;
|
|
43
|
-
result: string;
|
|
44
|
-
detail: string;
|
|
45
|
-
} | null;
|
|
42
|
+
export declare function checkForbiddenScope(changedFiles: ChangedFile[], forbidden: string[] | null): AuditCheck | null;
|
|
46
43
|
/**
|
|
47
44
|
* Check 2: done_evidence_missing (warn level).
|
|
48
45
|
* Returns null when no done_evidence list is configured (skip).
|
|
49
46
|
* Returns pass when all listed files exist in the task directory.
|
|
50
47
|
* Returns warn when any listed file is missing.
|
|
51
48
|
*/
|
|
52
|
-
export declare function checkDoneEvidenceMissing(taskDir:
|
|
53
|
-
name: string;
|
|
54
|
-
result: string;
|
|
55
|
-
detail: string;
|
|
56
|
-
} | null;
|
|
49
|
+
export declare function checkDoneEvidenceMissing(taskDir: string, doneEvidence: string[] | null): AuditCheck | null;
|
|
57
50
|
/**
|
|
58
51
|
* Check 3: readme_changelog_sync (warn level).
|
|
59
52
|
* Returns null when no code files (by extension) were changed (skip).
|
|
60
53
|
* Returns pass when README.md or CHANGELOG.md (basename, case-insensitive) is also changed.
|
|
61
54
|
* Returns warn when code changed but neither documentation file was updated.
|
|
62
55
|
*/
|
|
63
|
-
export declare function checkReadmeChangelogSync(changedFiles:
|
|
64
|
-
name: string;
|
|
65
|
-
result: string;
|
|
66
|
-
detail: string;
|
|
67
|
-
} | null;
|
|
56
|
+
export declare function checkReadmeChangelogSync(changedFiles: ChangedFile[]): AuditCheck | null;
|
|
68
57
|
/**
|
|
69
58
|
* Check 4: package_manifest_consistency (warn level).
|
|
70
59
|
* Returns null when package.json is not among changed files (skip).
|
|
71
60
|
* Returns pass when package.json parses as JSON and has non-empty name and version strings.
|
|
72
61
|
* Returns warn otherwise.
|
|
73
62
|
*/
|
|
74
|
-
export declare function checkPackageManifestConsistency(changedFiles:
|
|
75
|
-
name: string;
|
|
76
|
-
result: string;
|
|
77
|
-
detail: string;
|
|
78
|
-
} | null;
|
|
63
|
+
export declare function checkPackageManifestConsistency(changedFiles: ChangedFile[], repoPathSafe: string): AuditCheck | null;
|
|
79
64
|
/**
|
|
80
65
|
* Check 5: sensitive_path_access (fail level).
|
|
81
66
|
* Returns null when there are no changed files (skip).
|
|
82
67
|
* Returns pass when no changed file path (or old_path) is sensitive.
|
|
83
68
|
* Returns fail when any changed file path is flagged by isSensitivePath.
|
|
84
69
|
*/
|
|
85
|
-
export declare function checkSensitivePathAccess(changedFiles:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
detail: string;
|
|
89
|
-
} | null;
|
|
90
|
-
export declare function checkUnrecordedCommandExecution(testLogContent: string | null, resultMdContent: string | null, verifyCommands: string[], testCommand: string | null, repoPath?: string | null): {
|
|
91
|
-
name: string;
|
|
92
|
-
result: string;
|
|
93
|
-
detail: string;
|
|
94
|
-
} | null;
|
|
95
|
-
export declare function auditTask(taskId: any): {
|
|
96
|
-
task_id: any;
|
|
97
|
-
verdict: string;
|
|
98
|
-
acceptance: {
|
|
99
|
-
verdict: import("../goal/acceptanceEngine.js").AcceptanceVerdict;
|
|
100
|
-
status: string;
|
|
101
|
-
reason: string;
|
|
102
|
-
reasons: string[];
|
|
103
|
-
required_evidence: string[];
|
|
104
|
-
next_suggested_task: string;
|
|
105
|
-
fail_checks: import("../goal/acceptanceEngine.js").AcceptanceCheck[];
|
|
106
|
-
warn_checks: import("../goal/acceptanceEngine.js").AcceptanceCheck[];
|
|
107
|
-
};
|
|
108
|
-
summary: string;
|
|
109
|
-
checks: {
|
|
110
|
-
name: string;
|
|
111
|
-
result: string;
|
|
112
|
-
detail: string;
|
|
113
|
-
}[];
|
|
114
|
-
risks: {
|
|
115
|
-
severity: string;
|
|
116
|
-
description: string;
|
|
117
|
-
}[];
|
|
118
|
-
confirmed_failures: {
|
|
119
|
-
name: string;
|
|
120
|
-
result: string;
|
|
121
|
-
detail: string;
|
|
122
|
-
}[];
|
|
123
|
-
possible_false_positives: any[];
|
|
124
|
-
manual_verification_required: boolean;
|
|
125
|
-
manual_verification_items: any[];
|
|
126
|
-
recommended_next_actions: string[];
|
|
127
|
-
};
|
|
70
|
+
export declare function checkSensitivePathAccess(changedFiles: ChangedFile[]): AuditCheck | null;
|
|
71
|
+
export declare function checkUnrecordedCommandExecution(testLogContent: string | null, resultMdContent: string | null, verifyCommands: string[], testCommand: string | null, repoPath?: string | null): AuditCheck | null;
|
|
72
|
+
export declare function auditTask(taskId: string): AuditTaskOutput;
|
|
128
73
|
export {};
|
package/dist/tools/auditTask.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
import { readFileSync, existsSync, writeFileSync, readdirSync, statSync } from "node:fs";
|
|
3
2
|
import { join, resolve, relative, basename, sep, isAbsolute } from "node:path";
|
|
4
3
|
import { getTasksDir, getConfig } from "../config.js";
|
|
@@ -288,9 +287,10 @@ function readPackageScripts(repoPath) {
|
|
|
288
287
|
try {
|
|
289
288
|
guardSensitivePath(pkgJsonPath);
|
|
290
289
|
const parsed = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
291
|
-
|
|
290
|
+
const scripts = parsed.scripts;
|
|
291
|
+
if (!scripts || typeof scripts !== "object" || Array.isArray(scripts))
|
|
292
292
|
return {};
|
|
293
|
-
return Object.fromEntries(Object.entries(
|
|
293
|
+
return Object.fromEntries(Object.entries(scripts).filter(([, value]) => typeof value === "string"));
|
|
294
294
|
}
|
|
295
295
|
catch {
|
|
296
296
|
return {};
|
|
@@ -370,8 +370,8 @@ function findHighRiskCommandEvidence(text) {
|
|
|
370
370
|
return [...new Set(found)];
|
|
371
371
|
}
|
|
372
372
|
export function checkUnrecordedCommandExecution(testLogContent, resultMdContent, verifyCommands, testCommand, repoPath = null) {
|
|
373
|
-
const hasLog = testLogContent && testLogContent.length > 0;
|
|
374
|
-
const hasResult = resultMdContent && resultMdContent.length > 0;
|
|
373
|
+
const hasLog = !!testLogContent && testLogContent.length > 0;
|
|
374
|
+
const hasResult = !!resultMdContent && resultMdContent.length > 0;
|
|
375
375
|
if (!hasLog && !hasResult)
|
|
376
376
|
return null;
|
|
377
377
|
const combined = `${testLogContent || ""}\n${resultMdContent || ""}`;
|
|
@@ -381,9 +381,9 @@ export function checkUnrecordedCommandExecution(testLogContent, resultMdContent,
|
|
|
381
381
|
}
|
|
382
382
|
const whitelist = buildCommandWhitelist(verifyCommands, testCommand, repoPath);
|
|
383
383
|
const discovered = [];
|
|
384
|
-
if (hasLog)
|
|
384
|
+
if (hasLog && testLogContent)
|
|
385
385
|
discovered.push(...extractCommands(testLogContent));
|
|
386
|
-
if (hasResult)
|
|
386
|
+
if (hasResult && resultMdContent)
|
|
387
387
|
discovered.push(...extractCommands(resultMdContent));
|
|
388
388
|
const unrecorded = new Set();
|
|
389
389
|
for (const cmd of discovered) {
|
|
@@ -804,7 +804,11 @@ export function auditTask(taskId) {
|
|
|
804
804
|
return null;
|
|
805
805
|
try {
|
|
806
806
|
const v = JSON.parse(readFileSync(verifyJsonFile, "utf-8"));
|
|
807
|
-
|
|
807
|
+
if (v.status === "passed")
|
|
808
|
+
return "passed";
|
|
809
|
+
if (v.status === "failed")
|
|
810
|
+
return "failed";
|
|
811
|
+
return "skipped";
|
|
808
812
|
}
|
|
809
813
|
catch {
|
|
810
814
|
return null;
|
|
@@ -11,4 +11,4 @@ export interface CreateDirectSessionOutput {
|
|
|
11
11
|
expires_at: string;
|
|
12
12
|
next_action: string;
|
|
13
13
|
}
|
|
14
|
-
export declare function createDirectSession(input: CreateDirectSessionInput): CreateDirectSessionOutput
|
|
14
|
+
export declare function createDirectSession(input: CreateDirectSessionInput): Promise<CreateDirectSessionOutput>;
|
|
@@ -6,7 +6,7 @@ import { guardRuntimeSelfModification } from "../security/runtimeGuard.js";
|
|
|
6
6
|
import { captureRepoSnapshot } from "../runner/changeCapture.js";
|
|
7
7
|
import { createDirectSession as createDirectSessionRecord, } from "../direct/directSessionStore.js";
|
|
8
8
|
// ── Tool implementation ────────────────────────────────────────────
|
|
9
|
-
export function createDirectSession(input) {
|
|
9
|
+
export async function createDirectSession(input) {
|
|
10
10
|
const config = getConfig();
|
|
11
11
|
// ── Validate repo_path ───────────────────────────────────────────
|
|
12
12
|
if (!input.repo_path || input.repo_path.trim() === "") {
|
|
@@ -33,7 +33,7 @@ export function createDirectSession(input) {
|
|
|
33
33
|
// ── Runtime self-modification protection ─────────────────────────
|
|
34
34
|
guardRuntimeSelfModification(resolvedRepoPath);
|
|
35
35
|
// ── Capture repo snapshot ────────────────────────────────────────
|
|
36
|
-
const snapshot = captureRepoSnapshot(resolvedRepoPath);
|
|
36
|
+
const snapshot = await captureRepoSnapshot(resolvedRepoPath);
|
|
37
37
|
// ── Create session record ────────────────────────────────────────
|
|
38
38
|
const session = createDirectSessionRecord({
|
|
39
39
|
repo_path: input.repo_path,
|