opencode-goal-mode 0.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/LICENSE +21 -0
- package/README.md +121 -0
- package/agents/goal-api-reviewer.md +52 -0
- package/agents/goal-architect.md +52 -0
- package/agents/goal-commentator.md +45 -0
- package/agents/goal-completion-guard.md +35 -0
- package/agents/goal-coordinator.md +47 -0
- package/agents/goal-data-reviewer.md +51 -0
- package/agents/goal-deep-researcher.md +58 -0
- package/agents/goal-diff-reviewer.md +49 -0
- package/agents/goal-doc-reviewer.md +33 -0
- package/agents/goal-doc-writer.md +46 -0
- package/agents/goal-explorer.md +35 -0
- package/agents/goal-final-auditor.md +39 -0
- package/agents/goal-implementer.md +34 -0
- package/agents/goal-mapper.md +53 -0
- package/agents/goal-ops-reviewer.md +33 -0
- package/agents/goal-perf-reviewer.md +51 -0
- package/agents/goal-planner.md +40 -0
- package/agents/goal-prompt-auditor.md +39 -0
- package/agents/goal-quality-gate.md +51 -0
- package/agents/goal-researcher.md +34 -0
- package/agents/goal-reviewer.md +61 -0
- package/agents/goal-security-reviewer.md +33 -0
- package/agents/goal-test-reviewer.md +48 -0
- package/agents/goal-ux-reviewer.md +33 -0
- package/agents/goal-verifier.md +49 -0
- package/agents/goal-web-researcher.md +58 -0
- package/agents/goal.md +179 -0
- package/commands/goal-contract.md +14 -0
- package/commands/goal-final.md +15 -0
- package/commands/goal-repair.md +12 -0
- package/commands/goal-review.md +15 -0
- package/commands/goal-status.md +23 -0
- package/commands/goal.md +12 -0
- package/docs/research-report.md +37 -0
- package/package.json +61 -0
- package/plugins/goal-guard.js +426 -0
- package/scripts/check-npm-publish-ready.mjs +54 -0
- package/scripts/install.mjs +108 -0
- package/scripts/validate-opencode-config.mjs +82 -0
- package/tests/agents.test.mjs +70 -0
- package/tests/commands.test.mjs +23 -0
- package/tests/helpers.mjs +23 -0
- package/tests/install.test.mjs +64 -0
- package/tests/plugin.test.mjs +195 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { filesIn, readRepo, frontmatter, hasLine } from "./helpers.mjs";
|
|
4
|
+
|
|
5
|
+
const requiredAgents = [
|
|
6
|
+
"goal.md",
|
|
7
|
+
"goal-explorer.md",
|
|
8
|
+
"goal-researcher.md",
|
|
9
|
+
"goal-implementer.md",
|
|
10
|
+
"goal-reviewer.md",
|
|
11
|
+
"goal-prompt-auditor.md",
|
|
12
|
+
"goal-diff-reviewer.md",
|
|
13
|
+
"goal-verifier.md",
|
|
14
|
+
"goal-test-reviewer.md",
|
|
15
|
+
"goal-security-reviewer.md",
|
|
16
|
+
"goal-ux-reviewer.md",
|
|
17
|
+
"goal-ops-reviewer.md",
|
|
18
|
+
"goal-doc-reviewer.md",
|
|
19
|
+
"goal-final-auditor.md",
|
|
20
|
+
"goal-deep-researcher.md",
|
|
21
|
+
"goal-web-researcher.md",
|
|
22
|
+
"goal-architect.md",
|
|
23
|
+
"goal-mapper.md",
|
|
24
|
+
"goal-planner.md",
|
|
25
|
+
"goal-coordinator.md",
|
|
26
|
+
"goal-doc-writer.md",
|
|
27
|
+
"goal-commentator.md",
|
|
28
|
+
"goal-api-reviewer.md",
|
|
29
|
+
"goal-data-reviewer.md",
|
|
30
|
+
"goal-perf-reviewer.md",
|
|
31
|
+
"goal-quality-gate.md",
|
|
32
|
+
"goal-completion-guard.md",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
test("all required agents exist", () => {
|
|
36
|
+
const files = filesIn("agents");
|
|
37
|
+
for (const agent of requiredAgents) assert.ok(files.includes(agent), `${agent} missing`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("agents have required frontmatter", () => {
|
|
41
|
+
for (const file of requiredAgents) {
|
|
42
|
+
const fm = frontmatter(readRepo(`agents/${file}`));
|
|
43
|
+
assert.ok(hasLine(fm, "description"), `${file} missing description`);
|
|
44
|
+
assert.ok(hasLine(fm, "mode"), `${file} missing mode`);
|
|
45
|
+
assert.ok(hasLine(fm, "permission"), `${file} missing permission`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("primary goal enforces review artifacts and final contract", () => {
|
|
50
|
+
const text = readRepo("agents/goal.md");
|
|
51
|
+
for (const phrase of [
|
|
52
|
+
"Goal Contract",
|
|
53
|
+
"Verification Ledger",
|
|
54
|
+
"Review Ledger",
|
|
55
|
+
"Required review matrix",
|
|
56
|
+
"Review handoff template",
|
|
57
|
+
"Goal Completed",
|
|
58
|
+
"Review cycles: N",
|
|
59
|
+
]) {
|
|
60
|
+
assert.ok(text.includes(phrase), `goal.md missing ${phrase}`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("reviewers are read-only", () => {
|
|
65
|
+
for (const file of requiredAgents.filter((name) => name.includes("reviewer") || name.includes("auditor") || name.includes("verifier"))) {
|
|
66
|
+
const fm = frontmatter(readRepo(`agents/${file}`));
|
|
67
|
+
assert.match(fm, /edit:\s+deny/, `${file} must deny edit`);
|
|
68
|
+
assert.match(fm, /task:\s+deny/, `${file} must deny task nesting`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { filesIn, readRepo, frontmatter, hasLine } from "./helpers.mjs";
|
|
4
|
+
|
|
5
|
+
const requiredCommands = ["goal.md", "goal-contract.md", "goal-review.md", "goal-status.md", "goal-repair.md", "goal-final.md"];
|
|
6
|
+
|
|
7
|
+
test("all required commands exist", () => {
|
|
8
|
+
const files = filesIn("commands");
|
|
9
|
+
for (const command of requiredCommands) assert.ok(files.includes(command), `${command} missing`);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("commands have descriptions and agent bindings", () => {
|
|
13
|
+
for (const file of requiredCommands) {
|
|
14
|
+
const fm = frontmatter(readRepo(`commands/${file}`));
|
|
15
|
+
assert.ok(hasLine(fm, "description"), `${file} missing description`);
|
|
16
|
+
assert.ok(hasLine(fm, "agent"), `${file} missing agent`);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("review and final commands run as subtasks", () => {
|
|
21
|
+
assert.match(frontmatter(readRepo("commands/goal-review.md")), /subtask:\s+true/);
|
|
22
|
+
assert.match(frontmatter(readRepo("commands/goal-final.md")), /subtask:\s+true/);
|
|
23
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export const root = fileURLToPath(new URL("..", import.meta.url));
|
|
6
|
+
|
|
7
|
+
export function filesIn(dir) {
|
|
8
|
+
return readdirSync(join(root, dir)).filter((file) => file.endsWith(".md") || file.endsWith(".js"));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function readRepo(path) {
|
|
12
|
+
return readFileSync(join(root, path), "utf8");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function frontmatter(text) {
|
|
16
|
+
const match = text.match(/^---\n([\s\S]*?)\n---\n/);
|
|
17
|
+
if (!match) throw new Error("Missing YAML frontmatter");
|
|
18
|
+
return match[1];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function hasLine(fm, key) {
|
|
22
|
+
return new RegExp(`^${key}:`, "m").test(fm);
|
|
23
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { readRepo } from "./helpers.mjs";
|
|
9
|
+
|
|
10
|
+
const repoRoot = fileURLToPath(new URL("..", import.meta.url));
|
|
11
|
+
|
|
12
|
+
test("installer copies only safe OpenCode component directories", () => {
|
|
13
|
+
const text = readRepo("scripts/install.mjs");
|
|
14
|
+
assert.match(text, /copyDirFiles\(join\(root, "agents"\)/);
|
|
15
|
+
assert.match(text, /copyDirFiles\(join\(root, "commands"\)/);
|
|
16
|
+
assert.match(text, /copyDirFiles\(join\(root, "plugins"\)/);
|
|
17
|
+
assert.doesNotMatch(text, /auth|sessions|preauth|failures|hosts\.yml/);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("gitignore excludes secrets and dependencies", () => {
|
|
21
|
+
const text = readRepo(".gitignore");
|
|
22
|
+
for (const pattern of ["node_modules/", ".env", ".env.*"]) assert.ok(text.includes(pattern));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("installer functionally copies components to project .opencode", () => {
|
|
26
|
+
const temp = mkdtempSync(join(tmpdir(), "goal-install-"));
|
|
27
|
+
execFileSync("node", [join(repoRoot, "scripts", "install.mjs")], { cwd: temp, stdio: "pipe" });
|
|
28
|
+
assert.equal(existsSync(join(temp, ".opencode", "agents", "goal.md")), true);
|
|
29
|
+
assert.equal(existsSync(join(temp, ".opencode", "commands", "goal.md")), true);
|
|
30
|
+
assert.equal(existsSync(join(temp, ".opencode", "plugins", "goal-guard.js")), true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("installer dry run does not write files", () => {
|
|
34
|
+
const temp = mkdtempSync(join(tmpdir(), "goal-install-dry-"));
|
|
35
|
+
execFileSync("node", [join(repoRoot, "scripts", "install.mjs"), "--dry-run"], { cwd: temp, stdio: "pipe" });
|
|
36
|
+
assert.equal(existsSync(join(temp, ".opencode")), false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("installer refuses to overwrite changed destination files", () => {
|
|
40
|
+
const temp = mkdtempSync(join(tmpdir(), "goal-install-conflict-"));
|
|
41
|
+
mkdirSync(join(temp, ".opencode", "agents"), { recursive: true });
|
|
42
|
+
writeFileSync(join(temp, ".opencode", "agents", "goal.md"), "local change\n");
|
|
43
|
+
assert.throws(
|
|
44
|
+
() => execFileSync("node", [join(repoRoot, "scripts", "install.mjs")], { cwd: temp, stdio: "pipe" }),
|
|
45
|
+
/Refusing to overwrite changed OpenCode component files/
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("installer force replaces changed destination files", () => {
|
|
50
|
+
const temp = mkdtempSync(join(tmpdir(), "goal-install-force-"));
|
|
51
|
+
mkdirSync(join(temp, ".opencode", "agents"), { recursive: true });
|
|
52
|
+
const dest = join(temp, ".opencode", "agents", "goal.md");
|
|
53
|
+
writeFileSync(dest, "local change\n");
|
|
54
|
+
execFileSync("node", [join(repoRoot, "scripts", "install.mjs"), "--force"], { cwd: temp, stdio: "pipe" });
|
|
55
|
+
assert.equal(readFileSync(dest, "utf8"), readFileSync(join(repoRoot, "agents", "goal.md"), "utf8"));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("installer supports explicit target directory", () => {
|
|
59
|
+
const temp = mkdtempSync(join(tmpdir(), "goal-install-target-"));
|
|
60
|
+
const target = join(temp, "custom-opencode");
|
|
61
|
+
execFileSync("node", [join(repoRoot, "scripts", "install.mjs"), "--target", target], { cwd: temp, stdio: "pipe" });
|
|
62
|
+
assert.equal(existsSync(join(target, "agents", "goal.md")), true);
|
|
63
|
+
assert.equal(existsSync(join(target, "plugins", "goal-guard.js")), true);
|
|
64
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import plugin, { __test } from "../plugins/goal-guard.js";
|
|
4
|
+
|
|
5
|
+
test("detects destructive bash commands", () => {
|
|
6
|
+
assert.equal(__test.looksLikeDestructiveBash("rm -rf /tmp/x"), true);
|
|
7
|
+
assert.equal(__test.looksLikeDestructiveBash("sudo rm -fr /tmp/x"), true);
|
|
8
|
+
assert.equal(__test.looksLikeDestructiveBash("rm --recursive --force /tmp/x"), true);
|
|
9
|
+
assert.equal(__test.looksLikeDestructiveBash("git reset --hard"), true);
|
|
10
|
+
assert.equal(__test.looksLikeDestructiveBash("git push --force"), true);
|
|
11
|
+
assert.equal(__test.looksLikeDestructiveBash("find . -delete"), true);
|
|
12
|
+
assert.equal(__test.looksLikeDestructiveBash("find . -exec rm -f {} +"), true);
|
|
13
|
+
assert.equal(__test.looksLikeDestructiveBash("dd if=/tmp/x of=/dev/sda"), true);
|
|
14
|
+
assert.equal(__test.looksLikeDestructiveBash("npm test"), false);
|
|
15
|
+
assert.equal(__test.looksLikeDestructiveBash("ls -la"), false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("distinguishes read-only and mutating bash commands", () => {
|
|
19
|
+
assert.equal(__test.looksLikeMutatingBash("cat README.md"), false);
|
|
20
|
+
assert.equal(__test.looksLikeMutatingBash("rg goal agents"), false);
|
|
21
|
+
assert.equal(__test.looksLikeMutatingBash("node -e \"console.log('ok')\""), false);
|
|
22
|
+
assert.equal(__test.looksLikeMutatingBash("cat README.md > /tmp/goal-output.txt"), true);
|
|
23
|
+
assert.equal(__test.looksLikeMutatingBash("npm install"), true);
|
|
24
|
+
assert.equal(__test.looksLikeMutatingBash("npx prettier --write README.md"), true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("detects verification commands without generic test false positives", () => {
|
|
28
|
+
assert.equal(__test.isVerification("npm test"), true);
|
|
29
|
+
assert.equal(__test.isVerification("npm run validate"), true);
|
|
30
|
+
assert.equal(__test.isVerification("rg test README.md"), false);
|
|
31
|
+
assert.equal(__test.isVerification("node tests/plugin.test.mjs"), false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("state cache evicts at the configured session limit", () => {
|
|
35
|
+
__test.sessions.clear();
|
|
36
|
+
for (let i = 0; i < 205; i += 1) __test.stateFor(`session-${i}`);
|
|
37
|
+
assert.equal(__test.sessions.size, 200);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("plugin blocks destructive bash before tool execution", async () => {
|
|
41
|
+
const hooks = await plugin({ client: { app: { log: async () => undefined } } });
|
|
42
|
+
await assert.rejects(
|
|
43
|
+
() => hooks["tool.execute.before"]({ tool: "bash", sessionID: "s", callID: "c" }, { args: { command: "git clean -fd" } }),
|
|
44
|
+
/blocked/i,
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("write tool marks session dirty", async () => {
|
|
49
|
+
const hooks = await plugin({ client: { app: { log: async () => undefined } } });
|
|
50
|
+
await hooks["tool.execute.after"]({ tool: "edit", sessionID: "dirty-test", callID: "c", args: {} }, { output: "", title: "", metadata: {} });
|
|
51
|
+
const state = __test.stateFor("dirty-test");
|
|
52
|
+
assert.equal(state.dirty, true);
|
|
53
|
+
assert.equal(Boolean(state.lastEditAt), true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("read-only bash does not mark session dirty", async () => {
|
|
57
|
+
const hooks = await plugin({ client: { app: { log: async () => undefined } } });
|
|
58
|
+
const sessionID = "read-only-bash-test";
|
|
59
|
+
await hooks["chat.params"]({ sessionID, agent: "goal" }, {});
|
|
60
|
+
await hooks["tool.execute.after"]({ tool: "bash", sessionID, callID: "c", args: { command: "cat README.md" } }, { output: "", title: "", metadata: {} });
|
|
61
|
+
const state = __test.stateFor(sessionID);
|
|
62
|
+
assert.equal(state.dirty, false);
|
|
63
|
+
assert.equal(state.lastEditAt, null);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("mutating bash marks session dirty", async () => {
|
|
67
|
+
const hooks = await plugin({ client: { app: { log: async () => undefined } } });
|
|
68
|
+
const sessionID = "mutating-bash-test";
|
|
69
|
+
await hooks["chat.params"]({ sessionID, agent: "goal" }, {});
|
|
70
|
+
await hooks["tool.execute.after"]({ tool: "bash", sessionID, callID: "c", args: { command: "cat README.md > /tmp/goal-output.txt" } }, { output: "", title: "", metadata: {} });
|
|
71
|
+
const state = __test.stateFor(sessionID);
|
|
72
|
+
assert.equal(state.dirty, true);
|
|
73
|
+
assert.equal(Boolean(state.lastEditAt), true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("task tool captures review verdict from subagent", async () => {
|
|
77
|
+
const hooks = await plugin({ client: { app: { log: async () => undefined } } });
|
|
78
|
+
const sessionID = "task-review-test";
|
|
79
|
+
await hooks["chat.params"]({ sessionID, agent: "goal" }, {});
|
|
80
|
+
await hooks["tool.execute.after"]({ tool: "task", sessionID, callID: "c", args: { subagent_type: "goal-reviewer", prompt: "Review this." } }, { output: "Verdict: PASS", title: "", metadata: {} });
|
|
81
|
+
const state = __test.stateFor(sessionID);
|
|
82
|
+
assert.equal(state.verdicts.some((v) => v.agent === "goal-reviewer" && v.verdict === "PASS"), true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("task tool captures review failure from subagent", async () => {
|
|
86
|
+
const hooks = await plugin({ client: { app: { log: async () => undefined } } });
|
|
87
|
+
const sessionID = "task-review-fail-test";
|
|
88
|
+
await hooks["chat.params"]({ sessionID, agent: "goal" }, {});
|
|
89
|
+
await hooks["tool.execute.after"]({ tool: "task", sessionID, callID: "c", args: { subagent_type: "goal-final-auditor", prompt: "Audit this." } }, { output: "Verdict: FAIL", title: "", metadata: {} });
|
|
90
|
+
const state = __test.stateFor(sessionID);
|
|
91
|
+
assert.equal(state.verdicts.some((v) => v.agent === "goal-final-auditor" && v.verdict === "FAIL"), true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("verification command updates lastVerificationAt", async () => {
|
|
95
|
+
const hooks = await plugin({ client: { app: { log: async () => undefined } } });
|
|
96
|
+
const sessionID = "verify-time-test";
|
|
97
|
+
await hooks["chat.params"]({ sessionID, agent: "goal" }, {});
|
|
98
|
+
const before = new Date().toISOString();
|
|
99
|
+
await hooks["tool.execute.after"]({ tool: "bash", sessionID, callID: "c", args: { command: "npm test" } }, { output: "", title: "", metadata: {} });
|
|
100
|
+
const state = __test.stateFor(sessionID);
|
|
101
|
+
assert.equal(state.verificationSeen, true);
|
|
102
|
+
assert.ok(state.lastVerificationAt >= before);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("task-based final auditor records one review cycle", async () => {
|
|
106
|
+
const hooks = await plugin({ client: { app: { log: async () => undefined } } });
|
|
107
|
+
const sessionID = "task-final-cycle-test";
|
|
108
|
+
await hooks["chat.params"]({ sessionID, agent: "goal" }, {});
|
|
109
|
+
await hooks["tool.execute.after"]({ tool: "task", sessionID, callID: "c", args: { subagent_type: "goal-final-auditor", prompt: "Audit this." } }, { output: "Verdict: PASS", title: "", metadata: {} });
|
|
110
|
+
const state = __test.stateFor(sessionID);
|
|
111
|
+
assert.equal(state.reviewCycles, 1);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("final completion blocks claimed review cycles that do not match recorded", async () => {
|
|
115
|
+
const hooks = await plugin({ client: { app: { log: async () => undefined } } });
|
|
116
|
+
const sessionID = "cycles-block-test";
|
|
117
|
+
await hooks["chat.params"]({ sessionID, agent: "goal" }, {});
|
|
118
|
+
await hooks["tool.execute.after"]({ tool: "edit", sessionID, callID: "c", args: {} }, { output: "", title: "", metadata: {} });
|
|
119
|
+
// separate verification so review timestamps stay after it
|
|
120
|
+
await hooks["tool.execute.after"]({ tool: "bash", sessionID, callID: "v", args: { command: "npm test" } }, { output: "", title: "", metadata: {} });
|
|
121
|
+
|
|
122
|
+
for (const agent of ["goal-prompt-auditor", "goal-reviewer", "goal-diff-reviewer", "goal-verifier", "goal-final-auditor"]) {
|
|
123
|
+
await hooks["chat.params"]({ sessionID, agent }, {});
|
|
124
|
+
await hooks["tool.execute.after"]({ tool: "bash", sessionID, callID: agent, args: { command: "npm test" } }, { output: "Verdict: PASS", title: "", metadata: {} });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const output = { text: "Goal Completed\n\nReview cycles: 2" };
|
|
128
|
+
await hooks["experimental.text.complete"]({ sessionID, messageID: "m", partID: "p" }, output);
|
|
129
|
+
assert.match(output.text, /Goal Not Completed/);
|
|
130
|
+
assert.match(output.text, /do not match recorded/i);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("final completion blocks missing review cycles entirely", async () => {
|
|
134
|
+
const hooks = await plugin({ client: { app: { log: async () => undefined } } });
|
|
135
|
+
const sessionID = "zero-cycles-block-test";
|
|
136
|
+
await hooks["chat.params"]({ sessionID, agent: "goal" }, {});
|
|
137
|
+
await hooks["tool.execute.after"]({ tool: "edit", sessionID, callID: "c", args: {} }, { output: "", title: "", metadata: {} });
|
|
138
|
+
await hooks["tool.execute.after"]({ tool: "bash", sessionID, callID: "v", args: { command: "npm test" } }, { output: "", title: "", metadata: {} });
|
|
139
|
+
const output = { text: "Goal Completed\n\nReview cycles: 0" };
|
|
140
|
+
await hooks["experimental.text.complete"]({ sessionID, messageID: "m", partID: "p" }, output);
|
|
141
|
+
assert.match(output.text, /Goal Not Completed/);
|
|
142
|
+
assert.match(output.text, /no review cycles recorded/i);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("final completion blocks missing review cycles line", async () => {
|
|
146
|
+
const hooks = await plugin({ client: { app: { log: async () => undefined } } });
|
|
147
|
+
await hooks["chat.params"]({ sessionID: "missing-cycle-line-test", agent: "goal" }, {});
|
|
148
|
+
const output = { text: "Goal Completed\n\nDone." };
|
|
149
|
+
await hooks["experimental.text.complete"]({ sessionID: "missing-cycle-line-test", messageID: "m", partID: "p" }, output);
|
|
150
|
+
assert.match(output.text, /Goal Not Completed/);
|
|
151
|
+
assert.match(output.text, /missing required Review cycles line/i);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("final completion is rewritten when dirty", async () => {
|
|
155
|
+
const hooks = await plugin({ client: { app: { log: async () => undefined } } });
|
|
156
|
+
await hooks["tool.execute.after"]({ tool: "edit", sessionID: "complete-test", callID: "c", args: {} }, { output: "", title: "", metadata: {} });
|
|
157
|
+
const output = { text: "Goal Completed\n\nReview cycles: 1" };
|
|
158
|
+
await hooks["experimental.text.complete"]({ sessionID: "complete-test", messageID: "m", partID: "p" }, output);
|
|
159
|
+
assert.match(output.text, /Goal Not Completed/);
|
|
160
|
+
assert.match(output.text, /blocked completion/i);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("final completion is rewritten when required reviews never ran", async () => {
|
|
164
|
+
const hooks = await plugin({ client: { app: { log: async () => undefined } } });
|
|
165
|
+
await hooks["chat.params"]({ sessionID: "no-review-test", agent: "goal" }, {});
|
|
166
|
+
const output = { text: "Goal Completed\n\nReview cycles: 0" };
|
|
167
|
+
await hooks["experimental.text.complete"]({ sessionID: "no-review-test", messageID: "m", partID: "p" }, output);
|
|
168
|
+
assert.match(output.text, /Goal Not Completed/);
|
|
169
|
+
assert.match(output.text, /no review cycles recorded/i);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("completion is allowed only after all required gates pass after edit", async () => {
|
|
173
|
+
const hooks = await plugin({ client: { app: { log: async () => undefined } } });
|
|
174
|
+
const sessionID = "gate-pass-test";
|
|
175
|
+
await hooks["chat.params"]({ sessionID, agent: "goal" }, {});
|
|
176
|
+
await hooks["tool.execute.after"]({ tool: "edit", sessionID, callID: "c", args: {} }, { output: "", title: "", metadata: {} });
|
|
177
|
+
|
|
178
|
+
for (const agent of ["goal-prompt-auditor", "goal-reviewer", "goal-diff-reviewer", "goal-verifier", "goal-final-auditor"]) {
|
|
179
|
+
await hooks["chat.params"]({ sessionID, agent }, {});
|
|
180
|
+
await hooks["tool.execute.after"]({ tool: "bash", sessionID, callID: agent, args: { command: "npm test" } }, { output: "Verdict: PASS", title: "", metadata: {} });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const output = { text: "Goal Completed\n\nReview cycles: 1" };
|
|
184
|
+
await hooks["experimental.text.complete"]({ sessionID, messageID: "m", partID: "p" }, output);
|
|
185
|
+
assert.match(output.text, /Goal Completed/);
|
|
186
|
+
assert.doesNotMatch(output.text, /Goal Not Completed/);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("compaction preserves goal guard state", async () => {
|
|
190
|
+
const hooks = await plugin({ client: { app: { log: async () => undefined } } });
|
|
191
|
+
const output = { context: [] };
|
|
192
|
+
await hooks["experimental.session.compacting"]({ sessionID: "compact-test" }, output);
|
|
193
|
+
assert.match(output.context.join("\n"), /Goal Guard state/);
|
|
194
|
+
assert.match(output.context.join("\n"), /Review Ledger/);
|
|
195
|
+
});
|