opencode-workflow-guard 1.15.3 → 1.15.5
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.md +20 -4
- package/docs/installation.md +1 -1
- package/package.json +1 -1
- package/src/lib/project-memory.ts +16 -0
- package/test/test.mts +23 -0
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/opencode-workflow-guard)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](test/test.mts)
|
|
6
6
|
|
|
7
|
-
Deterministic policy and enforcement layer for OpenCode. Workflow Guard enforces
|
|
7
|
+
Deterministic policy and enforcement layer for OpenCode. Workflow Guard enforces safe engineering discipline through **hard plugin hooks**, not prompt instructions that LLMs can ignore, and pairs enforcement with a proactive workflow-tool surface for planning, review, worktree isolation, technical debt, and policy simulation.
|
|
8
8
|
|
|
9
9
|
Workflow Guard is deliberately **not an agent harness**: it can constrain actions, record and explain decisions, and supply bounded context, but it does not plan, prioritize, delegate, or autonomously sequence the agent's work. Bounded continuation may resume an existing session with unfinished owned todos; it never chooses the next task or creates new work.
|
|
10
10
|
|
|
@@ -34,7 +34,7 @@ For the optional TUI badge, configure `"opencode-workflow-guard"` in `tui.json`.
|
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
36
|
npm run typecheck # Strict TypeScript check (0 errors)
|
|
37
|
-
npm test # Run
|
|
37
|
+
npm test # Run 1100+ unit and adversarial tests
|
|
38
38
|
npm run test:all # Typecheck + unit + install/load checks
|
|
39
39
|
WORKFLOW_GUARD_LIVE_E2E=1 npm run test:install # Also run policy probes with OpenCode's most recently selected model
|
|
40
40
|
```
|
|
@@ -54,11 +54,27 @@ For complete policy specifications and override rules, see [docs/policies.md](do
|
|
|
54
54
|
|
|
55
55
|
---
|
|
56
56
|
|
|
57
|
+
## Proactive Workflow Tools
|
|
58
|
+
|
|
59
|
+
Enforcement is only half the surface. The plugin also registers first-class agent tools (same tool names on OpenCode 1.x and 2.x) so disciplined workflow is the path of least resistance:
|
|
60
|
+
|
|
61
|
+
* **Task Planning:** `guard_next_tasks` surfaces durable repository planning context (`TODO.md`, `ROADMAP.md`, `PLAN.md`, `TASKS.md`, `BACKLOG.md`, their `docs/` counterparts, and plans under `docs/plans/`) at session start or during planning; `guard_status` reports active guardrails, branch protection, mutation counts, outstanding verification/review gates, a read-only git-hygiene snapshot, and the loaded `pluginVersion`.
|
|
62
|
+
* **Review & Verification:** before PR creation, a secondary reviewer subagent evaluates the diff against `guard_review_rubric` (five core axes: test integrity, task completeness, cleanliness, security, platform) and records its verdict with `record_review`; approvals are bound to the reviewed worktree content.
|
|
63
|
+
* **Worktree Isolation:** `guard_worktree_create` and `guard_worktree_cleanup` give parallel subagents isolated linked worktrees so concurrent mutations never collide.
|
|
64
|
+
* **Technical Debt:** `guard_review_followups` lists open P2/P3 durable review follow-ups; `guard_review_followup_resolve` closes them after the underlying issue is fixed and verified.
|
|
65
|
+
* **Policy Simulation:** `guard_why` returns the structured policy decision for a tool call or command without performing it; `guard_audit` shows recent audited policy entries.
|
|
66
|
+
* **Project Memory** (on by default): `project_memory_search`, `project_memory_record`, `project_memory_export`, and `project_memory_import` maintain durable facts, decisions, constraints, and lessons; secret content is rejected, and export promotes records to the repo-local `.opencode/memory/project-memory.jsonl`.
|
|
67
|
+
* **Socratic Learning** (off by default): `learning_checkpoint`, `learning_profile`, and `learning_record` maintain a global evidence-based learner profile with a per-session intervention budget.
|
|
68
|
+
|
|
69
|
+
The context modules — project memory, Socratic learning, and recovery checkpoints (`guard_recovery_restore`) — are independently gated and can be disabled without weakening policy enforcement; see [docs/installation.md](docs/installation.md). Full tool specifications live in [docs/policies.md](docs/policies.md#custom-tools).
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
57
73
|
## Documentation Index
|
|
58
74
|
|
|
59
75
|
| Guide | Description |
|
|
60
76
|
|---|---|
|
|
61
|
-
| [**Policy Reference**](docs/policies.md) | Comprehensive specification of all 24 enforced policies, invariants, and
|
|
77
|
+
| [**Policy Reference**](docs/policies.md) | Comprehensive specification of all 24 enforced policies, invariants, and the proactive tool surface (`guard_*`, `project_memory_*`, `learning_*`) |
|
|
62
78
|
| [**Installation & Configuration**](docs/installation.md) | Setup options, global vs. local install, worktree isolation, and project configuration |
|
|
63
79
|
| [**Managed Deployment**](docs/managed-deployment.md) | Administrator-managed OpenCode policy, platform locations, and startup diagnostics |
|
|
64
80
|
| [**Troubleshooting**](docs/troubleshooting.md) | Diagnosing policy blocks, common false positives, and emergency override procedures |
|
package/docs/installation.md
CHANGED
|
@@ -206,7 +206,7 @@ Project memory is optional supporting context, enabled by default, and requires
|
|
|
206
206
|
|
|
207
207
|
The plugin exposes `project_memory_search`, `project_memory_record`, `project_memory_export`, and `project_memory_import`. Recording is intended only for durable facts, decisions, constraints, and lessons; recognized secret content is rejected. Routine tool/test/verification output, transient errors and retries, successful operational telemetry, todos, and session progress belong in their existing operational stores rather than project memory.
|
|
208
208
|
|
|
209
|
-
Project-memory maintenance runs opportunistically when a project index is opened, at most once per day. Superseded memories and resolved review follow-ups are retained for 90 days and then pruned; current memories are not expired just because they are old. Maintenance checkpoints and compacts the active SQLite database. When the project-memory directory exceeds 100 MB, least-recently-used inactive project databases are evicted toward about 80 MB when eligible databases exist. The current project's database is never selected for global eviction, and maintenance failures do not disable core guard enforcement.
|
|
209
|
+
Project-memory maintenance runs opportunistically when a project index is opened, at most once per day. Superseded memories and resolved review follow-ups are retained for 90 days and then pruned; current memories are not expired just because they are old. Maintenance checkpoints and compacts the active SQLite database, and reaps coordination markers (`*.sqlite.active.*`) left behind by processes that died without closing their store; markers owned by live or unidentifiable processes are never removed. When the project-memory directory exceeds 100 MB, least-recently-used inactive project databases are evicted toward about 80 MB when eligible databases exist. The current project's database is never selected for global eviction, and maintenance failures do not disable core guard enforcement.
|
|
210
210
|
|
|
211
211
|
Portable project knowledge lives at the fixed path `.opencode/memory/project-memory.jsonl`. Importing that file bootstraps the local index, but exporting is always explicit and accepts selected record IDs rather than dumping private working memory. The plugin adds `.opencode/memory/` to `.git/info/exclude` for the local clone. To share promoted knowledge through Git, deliberately remove that local exclusion or force-add the JSONL file after reviewing its contents. Do not commit the SQLite database.
|
|
212
212
|
|
package/package.json
CHANGED
|
@@ -150,6 +150,21 @@ function databaseIsActive(path: string, directory: string): boolean {
|
|
|
150
150
|
return active;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
function reapStaleActiveMarkers(directory: string): void {
|
|
154
|
+
const databaseNames = new Set(readdirSync(directory).filter((entry) => entry.endsWith(".sqlite")));
|
|
155
|
+
for (const name of readdirSync(directory)) {
|
|
156
|
+
const markerSeparator = name.lastIndexOf(".active.");
|
|
157
|
+
if (markerSeparator < 0) continue;
|
|
158
|
+
const databaseName = name.slice(0, markerSeparator);
|
|
159
|
+
if (!databaseNames.has(databaseName)) continue;
|
|
160
|
+
const pid = Number(name.slice(markerSeparator + ".active.".length).split(".", 1)[0]);
|
|
161
|
+
if (!Number.isInteger(pid) || pid <= 0) continue;
|
|
162
|
+
try { process.kill(pid, 0); } catch (error) {
|
|
163
|
+
if ((error as NodeJS.ErrnoException).code === "ESRCH") rmSync(join(directory, name), { force: true });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
153
168
|
export function maintainProjectMemoryStorage(store: ProjectMemoryStore, directory = getProjectMemoryDir(), options: MaintenanceOptions = {}): void {
|
|
154
169
|
const now = options.now ?? Date.now();
|
|
155
170
|
const maintenanceMarker = `${store.path}.maintenance`;
|
|
@@ -173,6 +188,7 @@ export function maintainProjectMemoryStorage(store: ProjectMemoryStore, director
|
|
|
173
188
|
store.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
174
189
|
store.db.exec("PRAGMA optimize");
|
|
175
190
|
store.db.exec("VACUUM");
|
|
191
|
+
reapStaleActiveMarkers(directory);
|
|
176
192
|
try { utimesSync(store.path, now / 1000, now / 1000); } catch {}
|
|
177
193
|
writeFileSync(maintenanceMarker, "", { mode: 0o600 });
|
|
178
194
|
try { utimesSync(maintenanceMarker, now / 1000, now / 1000); } catch {}
|
package/test/test.mts
CHANGED
|
@@ -4878,6 +4878,29 @@ check("project memory retains newly superseded old records for the retention win
|
|
|
4878
4878
|
memoryDb.db.prepare("UPDATE memories SET superseded_at = ? WHERE id = ?").run(retentionNow - 91 * 24 * 60 * 60 * 1000, oldCurrentMemory.id);
|
|
4879
4879
|
maintainProjectMemoryStorage(memoryDb, memoryDir, { now: retentionNow + 1_000 });
|
|
4880
4880
|
check("project memory maintenance is throttled to once per day", memoryDb.db.prepare("SELECT 1 FROM memories WHERE id = ?").get(oldCurrentMemory.id) !== undefined);
|
|
4881
|
+
const reapDeadChild = spawnSync(process.execPath, ["-e", "process.exit(0)"], { stdio: "ignore" });
|
|
4882
|
+
const staleMarkerDb = join(memoryDir, "stale-marker-project.sqlite");
|
|
4883
|
+
writeFileSync(staleMarkerDb, "x".repeat(64));
|
|
4884
|
+
const staleDeadMarker = `${staleMarkerDb}.active.${reapDeadChild.pid}.dead`;
|
|
4885
|
+
const staleLiveMarker = `${staleMarkerDb}.active.${process.pid}.live`;
|
|
4886
|
+
const staleOpaqueMarker = `${staleMarkerDb}.active.opaque`;
|
|
4887
|
+
const trickyProjectDb = join(memoryDir, "tricky.sqlite.active.55.sqlite");
|
|
4888
|
+
const trickyDeadMarker = `${trickyProjectDb}.active.${reapDeadChild.pid}.dead`;
|
|
4889
|
+
const trickyLiveMarker = `${trickyProjectDb}.active.${process.pid}.live`;
|
|
4890
|
+
writeFileSync(staleDeadMarker, "");
|
|
4891
|
+
writeFileSync(staleLiveMarker, "");
|
|
4892
|
+
writeFileSync(staleOpaqueMarker, "");
|
|
4893
|
+
writeFileSync(trickyProjectDb, "x".repeat(32));
|
|
4894
|
+
writeFileSync(trickyDeadMarker, "");
|
|
4895
|
+
writeFileSync(trickyLiveMarker, "");
|
|
4896
|
+
maintainProjectMemoryStorage(memoryDb, memoryDir, { force: true, now: retentionNow + 25 * 60 * 60 * 1000 });
|
|
4897
|
+
check("project memory reaps markers of dead owners during maintenance without storage pressure", !existsSync(staleDeadMarker) && existsSync(staleLiveMarker) && existsSync(staleOpaqueMarker));
|
|
4898
|
+
check("project memory reap never removes a database file or live marker for tricky names", existsSync(trickyProjectDb) && !existsSync(trickyDeadMarker) && existsSync(trickyLiveMarker));
|
|
4899
|
+
rmSync(staleLiveMarker, { force: true });
|
|
4900
|
+
rmSync(staleOpaqueMarker, { force: true });
|
|
4901
|
+
rmSync(staleMarkerDb, { force: true });
|
|
4902
|
+
rmSync(trickyLiveMarker, { force: true });
|
|
4903
|
+
rmSync(trickyProjectDb, { force: true });
|
|
4881
4904
|
const privateFact = recordProjectMemory(memoryDb, { kind: "fact", content: "A transient local observation.", source: "agent" });
|
|
4882
4905
|
const portableConstraint = recordProjectMemory(memoryDb, { kind: "constraint", content: "Project memory committed to Git must be human-readable.", source: "user" });
|
|
4883
4906
|
const portablePath = join(root, ".opencode", "memory", "project-memory.jsonl");
|