opencode-codeops 2.1.0 → 2.1.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/CHANGELOG.md CHANGED
@@ -2,8 +2,29 @@
2
2
 
3
3
  All notable changes to CodeOps are recorded here.
4
4
 
5
+ ## 2.1.1 — 2026-10-09
6
+
7
+ ### Fixes
8
+
9
+ - plugin: stop deleting the shared scratch directory on unload
10
+
11
+ ### Tests
12
+
13
+ - plugin: pin the scratch-directory lifecycle contract (red phase)
14
+
15
+ ### Documentation
16
+
17
+ - changelog: add the 2.1.0 usage notes and complete the feature
18
+
5
19
  ## 2.1.0 — 2026-10-09
6
20
 
21
+ ### Usage
22
+
23
+ - Run the `analyze-agents` skill on demand for a project-level specialist check; it records
24
+ `codeops/specialist-check.json`, and `analyze-project` reports the coverage line.
25
+ - `exec-plan` records each ruling batch in `plans/<plan>/05-findings.md`, which feeds finding-area
26
+ recurrence in the check.
27
+
7
28
  ### Documentation
8
29
 
9
30
  - readme: list the analyze-agents skill and the on-demand detection flow
package/README.md CHANGED
@@ -145,11 +145,11 @@ The plugin also warns (non-blocking) if any tool attempts to edit `codeops/.code
145
145
 
146
146
  Every session also gets a workspace-hygiene guard: the plugin exports `CODEOPS_TMPDIR`, a scratch
147
147
  directory under the OS temp directory that belongs to the running OpenCode plugin instance. Skills
148
- and agents put temporary files there and delete them when the run completes; the plugin removes the
149
- directory when it unloads and sweeps directories abandoned by interrupted runs at startup, so
150
- CodeOps does not accumulate scratch data. Because OpenCode 2 does not expose a session id to shell
151
- hooks, concurrent sessions served by one instance share this directory. The full protocol is in
152
- `_shared/workspace-hygiene.md`.
148
+ and agents put temporary files there and delete them when the run completes; the plugin sweeps
149
+ directories abandoned by interrupted runs at startup (older than seven days), so CodeOps does not
150
+ accumulate scratch data. Because OpenCode 2 does not expose a session id to shell hooks, concurrent
151
+ sessions served by one instance share this directory, and no session's scratch is deleted while
152
+ others may still use it. The full protocol is in `_shared/workspace-hygiene.md`.
153
153
 
154
154
  ## Agent model configuration
155
155
 
@@ -51,7 +51,9 @@ keep scratch file names specific to the run to avoid collisions.
51
51
 
52
52
  ## Automatic safety net
53
53
 
54
- The CodeOps plugin exports `$CODEOPS_TMPDIR` for each runtime and removes that runtime's
55
- directory when the plugin unloads. At startup it also removes CodeOps temp directories older
56
- than seven days, so an interrupted run cannot leak storage forever. The safety net is a
57
- fallback, not a substitute: the run itself still performs the cleanup pass above.
54
+ The CodeOps plugin exports `$CODEOPS_TMPDIR` for each runtime. At startup it removes CodeOps
55
+ temp directories older than seven days, so an interrupted run cannot leak storage forever.
56
+ The plugin never deletes a runtime's directory while sessions may still be running — several
57
+ sessions can share one directory, so reclamation happens only through the age-based sweep.
58
+ The safety net is a fallback, not a substitute: the run itself still performs the cleanup
59
+ pass above.
@@ -33,6 +33,3 @@ export interface CleanStaleTmpDirsOptions {
33
33
 
34
34
  /** Remove session directories abandoned by interrupted or crashed runs. */
35
35
  export declare function cleanStaleTmpDirs(options?: CleanStaleTmpDirsOptions): string[]
36
-
37
- /** Remove exactly one session's temp directory. */
38
- export declare function removeSessionTmpDir(sessionID?: string, base?: string): boolean
@@ -9,8 +9,7 @@
9
9
  * - it computes the per-session directory the plugin exports as
10
10
  * `$CODEOPS_TMPDIR`, under a CodeOps-owned root inside the OS temp directory;
11
11
  * - it sweeps directories abandoned by interrupted runs once they are older
12
- * than a safety cap; and
13
- * - it removes exactly one session's directory when that session is deleted.
12
+ * than a safety cap.
14
13
  *
15
14
  * Safety is the whole point: every path derives from a sanitized session
16
15
  * identifier, the sweep walks only the CodeOps-owned root, symlinked entries
@@ -22,7 +21,7 @@
22
21
 
23
22
  import { lstatSync, mkdirSync, readdirSync, rmSync } from "node:fs"
24
23
  import { tmpdir } from "node:os"
25
- import { join, resolve, sep } from "node:path"
24
+ import { join, resolve } from "node:path"
26
25
 
27
26
  /**
28
27
  * Age after which a session directory is treated as abandoned.
@@ -147,34 +146,3 @@ export function cleanStaleTmpDirs({
147
146
 
148
147
  return removed
149
148
  }
150
-
151
- /**
152
- * Remove exactly one session's temp directory.
153
- *
154
- * Used when a session is deleted: its scratch can no longer be needed. The
155
- * removal refuses anything that is not a regular directory strictly inside
156
- * the CodeOps temp root, so a symlink or an unexpected path is left alone.
157
- *
158
- * @param sessionID - Session identifier whose directory should be removed
159
- * @param base - Base temp directory (injectable for tests)
160
- * @returns True when the directory was removed
161
- */
162
- export function removeSessionTmpDir(sessionID, base = tmpdir()) {
163
- const root = resolve(codeopsTmpRoot(base))
164
- const dir = sessionTmpDir(sessionID, base)
165
-
166
- if (!resolve(dir).startsWith(root + sep)) return false
167
-
168
- try {
169
- if (!lstatSync(dir).isDirectory()) return false
170
- } catch {
171
- return false
172
- }
173
-
174
- try {
175
- rmSync(dir, { recursive: true, force: true })
176
- return true
177
- } catch {
178
- return false
179
- }
180
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-codeops",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "Specification-first engineering for complex systems — CodeOps plugin for OpenCode",
5
5
  "type": "module",
6
6
  "engines": {
package/plugin/index.ts CHANGED
@@ -8,7 +8,6 @@ import { fileURLToPath } from "node:url"
8
8
  import {
9
9
  cleanStaleTmpDirs,
10
10
  ensureSessionTmpDir,
11
- removeSessionTmpDir,
12
11
  } from "../bin/lib/tmp-hygiene.mjs"
13
12
  import {
14
13
  appendEffortTrace,
@@ -70,9 +69,9 @@ const standardsText = `${codingStandards}\n\n${outputStyle}`
70
69
  // OpenCode 2 does not expose a session identifier to shell hooks, so the plugin
71
70
  // owns one scratch directory per plugin runtime instead of one per session.
72
71
  // Skills and subagents still receive it as CODEOPS_TMPDIR and delete their own
73
- // scratch; the plugin removes the runtime directory on unload and sweeps
74
- // directories abandoned by earlier runs. Two sessions served by the same
75
- // runtime share this directory; see _shared/workspace-hygiene.md.
72
+ // scratch; the startup sweep reclaims directories abandoned by earlier runs.
73
+ // Several sessions can share one runtime directory, so the plugin must never
74
+ // delete it on unload; see _shared/workspace-hygiene.md.
76
75
  // ---------------------------------------------------------------------------
77
76
  const runtimeID = `runtime-${randomUUID()}`
78
77
 
@@ -386,16 +385,12 @@ export default Plugin.define({
386
385
  })
387
386
 
388
387
  // ---------------------------------------------------------------------
389
- // Plugin cleanup: stop listening for events and remove the runtime's
390
- // scratch directory. Both are best effort.
388
+ // Plugin cleanup: stop listening for events. The runtime's scratch
389
+ // directory is deliberately left in place — other sessions may still be
390
+ // using it — and the startup sweep reclaims abandoned directories.
391
391
  // ---------------------------------------------------------------------
392
392
  return () => {
393
393
  controller.abort()
394
- try {
395
- removeSessionTmpDir(runtimeID)
396
- } catch {
397
- // Best effort only.
398
- }
399
394
  }
400
395
  },
401
396
  })
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Specification tests for the scratch-directory lifecycle.
3
+ *
4
+ * The plugin exports one scratch directory (`$CODEOPS_TMPDIR`) per plugin
5
+ * runtime, and every session served by that runtime shares it. Deleting the
6
+ * directory when a plugin instance unloads destroys in-flight scratch files of
7
+ * sessions that are still running, so the contract is: the plugin never
8
+ * deletes the shared directory; the age-based sweep reclaims abandoned
9
+ * directories instead.
10
+ *
11
+ * This is the regression oracle for that contract: a failure here means the
12
+ * repository reintroduced early deletion, never that the test is wrong.
13
+ *
14
+ * @module temp-lifecycle.spec.test
15
+ */
16
+
17
+ import assert from "node:assert/strict"
18
+ import { readFileSync } from "node:fs"
19
+ import { dirname, join, resolve } from "node:path"
20
+ import { describe, it } from "node:test"
21
+ import { fileURLToPath } from "node:url"
22
+
23
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..")
24
+
25
+ /**
26
+ * Read a repository file as UTF-8 text.
27
+ * @param {string} relativePath - Path relative to the repository root.
28
+ * @returns {string} The file contents.
29
+ */
30
+ function read(relativePath) {
31
+ return readFileSync(join(ROOT, relativePath), "utf8")
32
+ }
33
+
34
+ describe("scratch-directory lifecycle", () => {
35
+ it("keeps the plugin free of scratch-directory deletion", () => {
36
+ const plugin = read("plugin/index.ts")
37
+ assert.doesNotMatch(plugin, /removeSessionTmpDir/)
38
+ assert.match(plugin, /cleanStaleTmpDirs\(\)/)
39
+ assert.match(plugin, /controller\.abort\(\)/)
40
+ })
41
+
42
+ it("keeps the temp hygiene module free of exact-directory deletion", () => {
43
+ assert.doesNotMatch(read("bin/lib/tmp-hygiene.mjs"), /removeSessionTmpDir/)
44
+ assert.doesNotMatch(read("bin/lib/tmp-hygiene.d.mts"), /removeSessionTmpDir/)
45
+ })
46
+
47
+ it("documents the age-based sweep as the automatic cleanup", () => {
48
+ const hygiene = read("_shared/workspace-hygiene.md")
49
+ assert.doesNotMatch(hygiene, /removes that runtime's\s+directory when the plugin unloads/i)
50
+ assert.match(hygiene, /seven days/i)
51
+ const readme = read("README.md")
52
+ assert.doesNotMatch(readme, /removes the\s+directory when it unloads/i)
53
+ assert.match(readme, /sweeps\s+directories abandoned/i)
54
+ })
55
+ })