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 +21 -0
- package/README.md +5 -5
- package/_shared/workspace-hygiene.md +6 -4
- package/bin/lib/tmp-hygiene.d.mts +0 -3
- package/bin/lib/tmp-hygiene.mjs +2 -34
- package/package.json +1 -1
- package/plugin/index.ts +6 -11
- package/plugin/temp-lifecycle.spec.test.mjs +55 -0
- package/scripts/__pycache__/install_agents.cpython-312.pyc +0 -0
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
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
package/bin/lib/tmp-hygiene.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
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
|
|
74
|
-
//
|
|
75
|
-
//
|
|
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
|
|
390
|
-
//
|
|
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
|
+
})
|
|
Binary file
|