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.
- package/ARCHITECTURE.md +180 -0
- package/README.md +158 -52
- package/agents/goal-api-reviewer.md +0 -2
- package/agents/goal-architect.md +0 -2
- package/agents/goal-commentator.md +0 -2
- package/agents/goal-completion-guard.md +0 -2
- package/agents/goal-coordinator.md +0 -2
- package/agents/goal-data-reviewer.md +0 -2
- package/agents/goal-deep-researcher.md +0 -2
- package/agents/goal-diff-reviewer.md +0 -2
- package/agents/goal-doc-reviewer.md +0 -2
- package/agents/goal-doc-writer.md +0 -2
- package/agents/goal-explorer.md +9 -8
- package/agents/goal-final-auditor.md +0 -2
- package/agents/goal-implementer.md +0 -2
- package/agents/goal-mapper.md +0 -2
- package/agents/goal-ops-reviewer.md +0 -2
- package/agents/goal-perf-reviewer.md +0 -2
- package/agents/goal-planner.md +10 -5
- package/agents/goal-prompt-auditor.md +0 -2
- package/agents/goal-quality-gate.md +0 -2
- package/agents/goal-researcher.md +8 -7
- package/agents/goal-reviewer.md +0 -2
- package/agents/goal-security-reviewer.md +0 -2
- package/agents/goal-test-reviewer.md +0 -2
- package/agents/goal-ux-reviewer.md +0 -2
- package/agents/goal-verifier.md +0 -2
- package/agents/goal-web-researcher.md +0 -2
- package/agents/goal.md +9 -8
- package/package.json +13 -9
- package/plugins/goal-guard/agents.js +132 -0
- package/plugins/goal-guard/completion.js +64 -0
- package/plugins/goal-guard/config.js +87 -0
- package/plugins/goal-guard/events.js +65 -0
- package/plugins/goal-guard/gates.js +85 -0
- package/plugins/goal-guard/logger.js +36 -0
- package/plugins/goal-guard/persistence.js +122 -0
- package/plugins/goal-guard/shell.js +1159 -0
- package/plugins/goal-guard/state.js +182 -0
- package/plugins/goal-guard/summary.js +46 -0
- package/plugins/goal-guard/system.js +43 -0
- package/plugins/goal-guard/tools.js +129 -0
- package/plugins/goal-guard/verdicts.js +87 -0
- package/plugins/goal-guard.js +267 -379
- package/scripts/install.mjs +170 -36
- package/docs/research-report.md +0 -37
- package/scripts/check-npm-publish-ready.mjs +0 -54
- package/scripts/validate-opencode-config.mjs +0 -82
- package/tests/agents.test.mjs +0 -70
- package/tests/commands.test.mjs +0 -23
- package/tests/helpers.mjs +0 -23
- package/tests/install.test.mjs +0 -64
- package/tests/plugin.test.mjs +0 -195
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defensive wrappers around the OpenCode client. Every call is best-effort:
|
|
3
|
+
* a logging or toast failure must never propagate into a hook and break a turn.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function createLogger(client) {
|
|
7
|
+
const log = client?.app?.log?.bind(client.app);
|
|
8
|
+
const toast = client?.tui?.showToast?.bind(client.tui);
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
async info(message, extra) {
|
|
12
|
+
if (!log) return;
|
|
13
|
+
try {
|
|
14
|
+
await log({ body: { service: "goal-guard", level: "info", message, extra } });
|
|
15
|
+
} catch {
|
|
16
|
+
/* ignore */
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
async warn(message, extra) {
|
|
20
|
+
if (!log) return;
|
|
21
|
+
try {
|
|
22
|
+
await log({ body: { service: "goal-guard", level: "warn", message, extra } });
|
|
23
|
+
} catch {
|
|
24
|
+
/* ignore */
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
async toast(message, variant = "warning") {
|
|
28
|
+
if (!toast) return;
|
|
29
|
+
try {
|
|
30
|
+
await toast({ body: { message, variant } });
|
|
31
|
+
} catch {
|
|
32
|
+
/* ignore */
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable, crash-safe JSON persistence for guard state.
|
|
3
|
+
*
|
|
4
|
+
* OpenCode exposes no key/value store to plugins, and a plugin's in-memory
|
|
5
|
+
* state is lost on every restart. This module persists the guard's review
|
|
6
|
+
* ledger under the XDG state directory, namespaced by a hash of the project
|
|
7
|
+
* worktree, so a long Goal session survives a restart with its dirty flags,
|
|
8
|
+
* verdicts and review-cycle count intact.
|
|
9
|
+
*
|
|
10
|
+
* Writes are atomic (temp file + rename) and debounced so a burst of tool
|
|
11
|
+
* calls does not thrash the disk. All disk access is wrapped so that a
|
|
12
|
+
* read-only or sandboxed filesystem degrades to pure in-memory operation
|
|
13
|
+
* rather than crashing a tool call.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
import { mkdirSync, readFileSync, writeFileSync, renameSync, rmSync } from "node:fs";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
|
|
21
|
+
/** Resolve the base directory for guard state files. */
|
|
22
|
+
export function stateBaseDir(env = process.env) {
|
|
23
|
+
const xdg = env.XDG_STATE_HOME && env.XDG_STATE_HOME.trim();
|
|
24
|
+
const base = xdg || join(homedir(), ".local", "state");
|
|
25
|
+
return join(base, "opencode", "goal-guard");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Stable per-project key from the worktree/directory. */
|
|
29
|
+
export function projectKey(worktree) {
|
|
30
|
+
const input = String(worktree || "default");
|
|
31
|
+
return createHash("sha256").update(input).digest("hex").slice(0, 16);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a persistence handle for one project.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} opts
|
|
38
|
+
* @param {string} [opts.worktree] Project worktree root (namespacing key).
|
|
39
|
+
* @param {boolean} [opts.enabled=true]
|
|
40
|
+
* @param {number} [opts.debounceMs=400]
|
|
41
|
+
* @param {Record<string,string|undefined>} [opts.env]
|
|
42
|
+
* @param {(fn: () => void, ms: number) => any} [opts.setTimer] Injectable for tests.
|
|
43
|
+
* @param {(handle: any) => void} [opts.clearTimer]
|
|
44
|
+
*/
|
|
45
|
+
export function createPersistence({
|
|
46
|
+
worktree,
|
|
47
|
+
enabled = true,
|
|
48
|
+
debounceMs = 400,
|
|
49
|
+
env = process.env,
|
|
50
|
+
setTimer = (fn, ms) => setTimeout(fn, ms),
|
|
51
|
+
clearTimer = (h) => clearTimeout(h),
|
|
52
|
+
} = {}) {
|
|
53
|
+
const dir = stateBaseDir(env);
|
|
54
|
+
const file = join(dir, `${projectKey(worktree)}.json`);
|
|
55
|
+
const tmp = `${file}.tmp`;
|
|
56
|
+
let timer = null;
|
|
57
|
+
let pending = null;
|
|
58
|
+
let degraded = false;
|
|
59
|
+
|
|
60
|
+
function load() {
|
|
61
|
+
if (!enabled || degraded) return null;
|
|
62
|
+
try {
|
|
63
|
+
const raw = readFileSync(file, "utf8");
|
|
64
|
+
return JSON.parse(raw);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (err && err.code === "ENOENT") return null;
|
|
67
|
+
// Corrupt or unreadable file: ignore and start fresh.
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function writeNow(data) {
|
|
73
|
+
if (!enabled || degraded) return false;
|
|
74
|
+
try {
|
|
75
|
+
mkdirSync(dir, { recursive: true });
|
|
76
|
+
writeFileSync(tmp, JSON.stringify(data), "utf8");
|
|
77
|
+
renameSync(tmp, file);
|
|
78
|
+
return true;
|
|
79
|
+
} catch {
|
|
80
|
+
degraded = true; // Stop trying on a read-only/sandboxed FS.
|
|
81
|
+
try {
|
|
82
|
+
rmSync(tmp, { force: true });
|
|
83
|
+
} catch {
|
|
84
|
+
/* ignore */
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Debounced save: coalesces rapid mutations into one disk write. */
|
|
91
|
+
function save(getData) {
|
|
92
|
+
if (!enabled || degraded) return;
|
|
93
|
+
pending = getData;
|
|
94
|
+
if (timer) return;
|
|
95
|
+
timer = setTimer(() => {
|
|
96
|
+
timer = null;
|
|
97
|
+
const fn = pending;
|
|
98
|
+
pending = null;
|
|
99
|
+
if (fn) writeNow(fn());
|
|
100
|
+
}, debounceMs);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Synchronous flush (used on dispose / idle). */
|
|
104
|
+
function flush(getData) {
|
|
105
|
+
if (timer) {
|
|
106
|
+
clearTimer(timer);
|
|
107
|
+
timer = null;
|
|
108
|
+
}
|
|
109
|
+
const fn = pending || getData;
|
|
110
|
+
pending = null;
|
|
111
|
+
if (fn) return writeNow(fn());
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
file,
|
|
117
|
+
load,
|
|
118
|
+
save,
|
|
119
|
+
flush,
|
|
120
|
+
isDegraded: () => degraded,
|
|
121
|
+
};
|
|
122
|
+
}
|