opencode-goal-mode 0.1.0 → 0.2.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.
Files changed (53) hide show
  1. package/ARCHITECTURE.md +180 -0
  2. package/README.md +158 -52
  3. package/agents/goal-api-reviewer.md +0 -2
  4. package/agents/goal-architect.md +0 -2
  5. package/agents/goal-commentator.md +0 -2
  6. package/agents/goal-completion-guard.md +0 -2
  7. package/agents/goal-coordinator.md +0 -2
  8. package/agents/goal-data-reviewer.md +0 -2
  9. package/agents/goal-deep-researcher.md +0 -2
  10. package/agents/goal-diff-reviewer.md +0 -2
  11. package/agents/goal-doc-reviewer.md +0 -2
  12. package/agents/goal-doc-writer.md +0 -2
  13. package/agents/goal-explorer.md +9 -8
  14. package/agents/goal-final-auditor.md +0 -2
  15. package/agents/goal-implementer.md +0 -2
  16. package/agents/goal-mapper.md +0 -2
  17. package/agents/goal-ops-reviewer.md +0 -2
  18. package/agents/goal-perf-reviewer.md +0 -2
  19. package/agents/goal-planner.md +10 -5
  20. package/agents/goal-prompt-auditor.md +0 -2
  21. package/agents/goal-quality-gate.md +0 -2
  22. package/agents/goal-researcher.md +8 -7
  23. package/agents/goal-reviewer.md +0 -2
  24. package/agents/goal-security-reviewer.md +0 -2
  25. package/agents/goal-test-reviewer.md +0 -2
  26. package/agents/goal-ux-reviewer.md +0 -2
  27. package/agents/goal-verifier.md +0 -2
  28. package/agents/goal-web-researcher.md +0 -2
  29. package/agents/goal.md +9 -8
  30. package/package.json +13 -9
  31. package/plugins/goal-guard/agents.js +132 -0
  32. package/plugins/goal-guard/completion.js +64 -0
  33. package/plugins/goal-guard/config.js +87 -0
  34. package/plugins/goal-guard/events.js +65 -0
  35. package/plugins/goal-guard/gates.js +85 -0
  36. package/plugins/goal-guard/logger.js +36 -0
  37. package/plugins/goal-guard/persistence.js +122 -0
  38. package/plugins/goal-guard/shell.js +1159 -0
  39. package/plugins/goal-guard/state.js +182 -0
  40. package/plugins/goal-guard/summary.js +46 -0
  41. package/plugins/goal-guard/system.js +43 -0
  42. package/plugins/goal-guard/tools.js +129 -0
  43. package/plugins/goal-guard/verdicts.js +87 -0
  44. package/plugins/goal-guard.js +267 -379
  45. package/scripts/install.mjs +170 -36
  46. package/docs/research-report.md +0 -37
  47. package/scripts/check-npm-publish-ready.mjs +0 -54
  48. package/scripts/validate-opencode-config.mjs +0 -82
  49. package/tests/agents.test.mjs +0 -70
  50. package/tests/commands.test.mjs +0 -23
  51. package/tests/helpers.mjs +0 -23
  52. package/tests/install.test.mjs +0 -64
  53. package/tests/plugin.test.mjs +0 -195
@@ -1,195 +0,0 @@
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
- });