opencode-immune 1.0.49 → 1.0.51
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/dist/plugin.js +67 -67
- package/package.json +2 -1
package/dist/plugin.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
// .opencode/plugin.ts — opencode-immune plugin
|
|
3
2
|
// Hybrid single-file architecture with factory functions, explicit state, error boundaries
|
|
4
3
|
// See: memory-bank/creative/creative-plugin-architecture.md (Option C)
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
import { createOpencodeClient as createOpencodeClientV2 } from "@opencode-ai/sdk/v2/client";
|
|
5
|
+
import { appendFile, mkdir, readFile, unlink, writeFile, stat, rm, rename, readdir, copyFile } from "fs/promises";
|
|
6
|
+
import { join, dirname } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { createHash } from "crypto";
|
|
9
|
+
import { tmpdir } from "os";
|
|
10
|
+
import { execFile } from "child_process";
|
|
12
11
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
13
12
|
// PLUGIN VERSION CHECK
|
|
14
13
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
15
|
-
const PLUGIN_VERSION = "1.0.
|
|
14
|
+
const PLUGIN_VERSION = "1.0.51";
|
|
16
15
|
const PLUGIN_PACKAGE_NAME = "opencode-immune";
|
|
16
|
+
const PLUGIN_DIRNAME = dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
/**
|
|
18
18
|
* Read plugin version from package.json at runtime.
|
|
19
19
|
* Falls back to PLUGIN_VERSION constant if read fails.
|
|
@@ -24,12 +24,12 @@ async function getPluginVersion() {
|
|
|
24
24
|
// dist/plugin.js → ../package.json
|
|
25
25
|
// Also try direct path for when loaded from npm cache.
|
|
26
26
|
const candidates = [
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
join(PLUGIN_DIRNAME, "..", "package.json"),
|
|
28
|
+
join(PLUGIN_DIRNAME, "package.json"),
|
|
29
29
|
];
|
|
30
30
|
for (const pkgPath of candidates) {
|
|
31
31
|
try {
|
|
32
|
-
const content = await
|
|
32
|
+
const content = await readFile(pkgPath, "utf-8");
|
|
33
33
|
const pkg = JSON.parse(content);
|
|
34
34
|
if (pkg.version)
|
|
35
35
|
return pkg.version;
|
|
@@ -77,7 +77,7 @@ function createState(input) {
|
|
|
77
77
|
const { client: _client, ...runtimeInput } = input;
|
|
78
78
|
return {
|
|
79
79
|
input: runtimeInput,
|
|
80
|
-
client: (
|
|
80
|
+
client: createOpencodeClientV2({
|
|
81
81
|
baseUrl: input.serverUrl.toString(),
|
|
82
82
|
directory: input.directory,
|
|
83
83
|
}),
|
|
@@ -87,8 +87,8 @@ function createState(input) {
|
|
|
87
87
|
providerRetryWatchdogs: new Map(),
|
|
88
88
|
childFallbackRequests: new Map(),
|
|
89
89
|
sessionErrorRetryCount: new Map(),
|
|
90
|
-
ultraworkMarkerPath:
|
|
91
|
-
diagnosticsLogPath:
|
|
90
|
+
ultraworkMarkerPath: join(input.directory, ".opencode", "state", "ultrawork-active.json"),
|
|
91
|
+
diagnosticsLogPath: join(input.directory, ".opencode", "state", "opencode-immune-debug.log"),
|
|
92
92
|
lastEditAttempt: null,
|
|
93
93
|
toolCallCount: 0,
|
|
94
94
|
todoWriteUsed: false,
|
|
@@ -188,11 +188,11 @@ function pruneExpiredManagedSessions(state, now = Date.now()) {
|
|
|
188
188
|
}
|
|
189
189
|
async function writeDiagnosticLog(state, event, data = {}) {
|
|
190
190
|
try {
|
|
191
|
-
const cacheDir =
|
|
192
|
-
await
|
|
191
|
+
const cacheDir = join(state.input.directory, ".opencode", "state");
|
|
192
|
+
await mkdir(cacheDir, { recursive: true });
|
|
193
193
|
await rotateDiagnosticLogIfNeeded(state.diagnosticsLogPath);
|
|
194
194
|
const line = JSON.stringify({ ts: new Date().toISOString(), event, ...data });
|
|
195
|
-
await
|
|
195
|
+
await appendFile(state.diagnosticsLogPath, `${line}\n`, "utf-8");
|
|
196
196
|
}
|
|
197
197
|
catch {
|
|
198
198
|
// diagnostics must never affect runtime behavior
|
|
@@ -200,12 +200,12 @@ async function writeDiagnosticLog(state, event, data = {}) {
|
|
|
200
200
|
}
|
|
201
201
|
async function rotateDiagnosticLogIfNeeded(logPath) {
|
|
202
202
|
try {
|
|
203
|
-
const current = await
|
|
203
|
+
const current = await stat(logPath);
|
|
204
204
|
if (current.size < DIAGNOSTIC_LOG_MAX_BYTES)
|
|
205
205
|
return;
|
|
206
206
|
const rotatedPath = `${logPath}.1`;
|
|
207
|
-
await
|
|
208
|
-
await
|
|
207
|
+
await rm(rotatedPath, { force: true });
|
|
208
|
+
await rename(logPath, rotatedPath);
|
|
209
209
|
}
|
|
210
210
|
catch {
|
|
211
211
|
// missing log or rotation failure must never affect runtime behavior
|
|
@@ -228,10 +228,10 @@ function writePluginLog(state, level, message, extra = {}) {
|
|
|
228
228
|
});
|
|
229
229
|
}
|
|
230
230
|
function writePluginLogForDirectory(directory, level, message, extra = {}) {
|
|
231
|
-
const diagnosticsLogPath =
|
|
231
|
+
const diagnosticsLogPath = join(directory, ".opencode", "state", "opencode-immune-debug.log");
|
|
232
232
|
void (async () => {
|
|
233
233
|
try {
|
|
234
|
-
await
|
|
234
|
+
await mkdir(dirname(diagnosticsLogPath), { recursive: true });
|
|
235
235
|
await rotateDiagnosticLogIfNeeded(diagnosticsLogPath);
|
|
236
236
|
const line = JSON.stringify({
|
|
237
237
|
ts: new Date().toISOString(),
|
|
@@ -239,7 +239,7 @@ function writePluginLogForDirectory(directory, level, message, extra = {}) {
|
|
|
239
239
|
message,
|
|
240
240
|
...extra,
|
|
241
241
|
});
|
|
242
|
-
await
|
|
242
|
+
await appendFile(diagnosticsLogPath, `${line}\n`, "utf-8");
|
|
243
243
|
}
|
|
244
244
|
catch {
|
|
245
245
|
// file logging must never affect runtime behavior
|
|
@@ -263,13 +263,13 @@ const pluginLog = {
|
|
|
263
263
|
// ── Ultrawork Marker File ──
|
|
264
264
|
async function writeUltraworkMarker(state) {
|
|
265
265
|
try {
|
|
266
|
-
const dir =
|
|
267
|
-
await
|
|
266
|
+
const dir = join(state.input.directory, ".opencode", "state");
|
|
267
|
+
await mkdir(dir, { recursive: true });
|
|
268
268
|
const payload = JSON.stringify({
|
|
269
269
|
active: true,
|
|
270
270
|
updatedAt: new Date().toISOString(),
|
|
271
271
|
});
|
|
272
|
-
await
|
|
272
|
+
await writeFile(state.ultraworkMarkerPath, payload, "utf-8");
|
|
273
273
|
}
|
|
274
274
|
catch {
|
|
275
275
|
// marker write must never affect runtime
|
|
@@ -277,7 +277,7 @@ async function writeUltraworkMarker(state) {
|
|
|
277
277
|
}
|
|
278
278
|
async function clearUltraworkMarker(state) {
|
|
279
279
|
try {
|
|
280
|
-
await
|
|
280
|
+
await unlink(state.ultraworkMarkerPath);
|
|
281
281
|
}
|
|
282
282
|
catch {
|
|
283
283
|
// file may not exist — that's fine
|
|
@@ -285,7 +285,7 @@ async function clearUltraworkMarker(state) {
|
|
|
285
285
|
}
|
|
286
286
|
async function isUltraworkMarkerActive(state) {
|
|
287
287
|
try {
|
|
288
|
-
const raw = await
|
|
288
|
+
const raw = await readFile(state.ultraworkMarkerPath, "utf-8");
|
|
289
289
|
const parsed = JSON.parse(raw);
|
|
290
290
|
return parsed?.active === true;
|
|
291
291
|
}
|
|
@@ -742,8 +742,8 @@ function compositeChatMessage(handlers) {
|
|
|
742
742
|
*/
|
|
743
743
|
async function parseTasksFile(directory) {
|
|
744
744
|
try {
|
|
745
|
-
const tasksPath =
|
|
746
|
-
const content = await
|
|
745
|
+
const tasksPath = join(directory, "memory-bank", "tasks.md");
|
|
746
|
+
const content = await readFile(tasksPath, "utf-8");
|
|
747
747
|
// Check for active task
|
|
748
748
|
if (!content.includes("## Active Task") ||
|
|
749
749
|
content.includes("No active tasks")) {
|
|
@@ -815,7 +815,7 @@ const DEFAULT_HARNESS_REPO = "gendoor/opencode-immune-harness";
|
|
|
815
815
|
async function parseDotEnv(filePath) {
|
|
816
816
|
const result = {};
|
|
817
817
|
try {
|
|
818
|
-
const content = await
|
|
818
|
+
const content = await readFile(filePath, "utf-8");
|
|
819
819
|
for (const line of content.split("\n")) {
|
|
820
820
|
const trimmed = line.trim();
|
|
821
821
|
if (!trimmed || trimmed.startsWith("#"))
|
|
@@ -849,13 +849,13 @@ async function resolveEnvValue(directory, key) {
|
|
|
849
849
|
if (process.env[key])
|
|
850
850
|
return process.env[key];
|
|
851
851
|
// 2. Per-project .env
|
|
852
|
-
const projectEnv = await parseDotEnv(
|
|
852
|
+
const projectEnv = await parseDotEnv(join(directory, ".env"));
|
|
853
853
|
if (projectEnv[key])
|
|
854
854
|
return projectEnv[key];
|
|
855
855
|
// 3. Global config
|
|
856
856
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
857
857
|
if (home) {
|
|
858
|
-
const globalEnv = await parseDotEnv(
|
|
858
|
+
const globalEnv = await parseDotEnv(join(home, ".config", "opencode-immune", ".env"));
|
|
859
859
|
if (globalEnv[key])
|
|
860
860
|
return globalEnv[key];
|
|
861
861
|
}
|
|
@@ -913,8 +913,8 @@ async function fetchLatestHarnessRelease(directory, repo, token) {
|
|
|
913
913
|
*/
|
|
914
914
|
async function readLocalHarnessVersion(directory) {
|
|
915
915
|
try {
|
|
916
|
-
const versionPath =
|
|
917
|
-
const content = await
|
|
916
|
+
const versionPath = join(directory, ".opencode", HARNESS_VERSION_FILE);
|
|
917
|
+
const content = await readFile(versionPath, "utf-8");
|
|
918
918
|
return content.trim() || null;
|
|
919
919
|
}
|
|
920
920
|
catch {
|
|
@@ -938,8 +938,8 @@ async function downloadHarnessAsset(assetUrl, token) {
|
|
|
938
938
|
throw new Error(`Download failed: ${resp.status} ${resp.statusText}`);
|
|
939
939
|
}
|
|
940
940
|
const buffer = Buffer.from(await resp.arrayBuffer());
|
|
941
|
-
const tempPath =
|
|
942
|
-
await
|
|
941
|
+
const tempPath = join(tmpdir(), `harness-${Date.now()}.tar.gz`);
|
|
942
|
+
await writeFile(tempPath, buffer);
|
|
943
943
|
return tempPath;
|
|
944
944
|
}
|
|
945
945
|
/**
|
|
@@ -948,7 +948,7 @@ async function downloadHarnessAsset(assetUrl, token) {
|
|
|
948
948
|
*/
|
|
949
949
|
function extractTarGz(archivePath, destDir) {
|
|
950
950
|
return new Promise((resolve, reject) => {
|
|
951
|
-
|
|
951
|
+
execFile("tar", ["xzf", archivePath, "-C", destDir], (err) => {
|
|
952
952
|
if (err)
|
|
953
953
|
reject(err);
|
|
954
954
|
else
|
|
@@ -963,22 +963,22 @@ function extractTarGz(archivePath, destDir) {
|
|
|
963
963
|
*/
|
|
964
964
|
async function copyDirRecursive(src, dest, skipRootFiles, rootDest) {
|
|
965
965
|
const effectiveRoot = rootDest ?? dest;
|
|
966
|
-
const entries = await
|
|
967
|
-
await
|
|
966
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
967
|
+
await mkdir(dest, { recursive: true });
|
|
968
968
|
for (const entry of entries) {
|
|
969
969
|
// Skip files only at the root destination level
|
|
970
970
|
if (skipRootFiles && dest === effectiveRoot && entry.name === ".gitignore") {
|
|
971
971
|
pluginLog.info(`[opencode-immune] Harness sync: skipping root .gitignore`);
|
|
972
972
|
continue;
|
|
973
973
|
}
|
|
974
|
-
const srcPath =
|
|
975
|
-
const destPath =
|
|
974
|
+
const srcPath = join(src, entry.name);
|
|
975
|
+
const destPath = join(dest, entry.name);
|
|
976
976
|
if (entry.isDirectory()) {
|
|
977
977
|
await copyDirRecursive(srcPath, destPath, skipRootFiles, effectiveRoot);
|
|
978
978
|
}
|
|
979
979
|
else {
|
|
980
|
-
await
|
|
981
|
-
await
|
|
980
|
+
await mkdir(dirname(destPath), { recursive: true });
|
|
981
|
+
await copyFile(srcPath, destPath);
|
|
982
982
|
}
|
|
983
983
|
}
|
|
984
984
|
}
|
|
@@ -987,8 +987,8 @@ async function copyDirRecursive(src, dest, skipRootFiles, rootDest) {
|
|
|
987
987
|
*/
|
|
988
988
|
async function fileHash(filePath) {
|
|
989
989
|
try {
|
|
990
|
-
const content = await
|
|
991
|
-
return
|
|
990
|
+
const content = await readFile(filePath);
|
|
991
|
+
return createHash("sha256").update(content).digest("hex");
|
|
992
992
|
}
|
|
993
993
|
catch {
|
|
994
994
|
return "";
|
|
@@ -1030,13 +1030,13 @@ async function syncHarness(state) {
|
|
|
1030
1030
|
}
|
|
1031
1031
|
pluginLog.info(`[opencode-immune] Harness sync: updating ${localVersion ?? "(none)"} → ${release.tagName}`);
|
|
1032
1032
|
// 3. Hash opencode.json before update
|
|
1033
|
-
const configPath =
|
|
1033
|
+
const configPath = join(state.input.directory, "opencode.json");
|
|
1034
1034
|
const hashBefore = await fileHash(configPath);
|
|
1035
1035
|
// 4. Download asset
|
|
1036
1036
|
const archivePath = await downloadHarnessAsset(release.assetUrl, token);
|
|
1037
1037
|
// 5. Extract to temp dir
|
|
1038
|
-
const extractDir =
|
|
1039
|
-
await
|
|
1038
|
+
const extractDir = join(tmpdir(), `harness-extract-${Date.now()}`);
|
|
1039
|
+
await mkdir(extractDir, { recursive: true });
|
|
1040
1040
|
try {
|
|
1041
1041
|
await extractTarGz(archivePath, extractDir);
|
|
1042
1042
|
// 6. Copy extracted files to project root (skip .gitignore — project owns its own)
|
|
@@ -1044,9 +1044,9 @@ async function syncHarness(state) {
|
|
|
1044
1044
|
const SKIP_ROOT_FILES = new Set([".gitignore"]);
|
|
1045
1045
|
await copyDirRecursive(extractDir, state.input.directory, SKIP_ROOT_FILES);
|
|
1046
1046
|
// 7. Write version marker
|
|
1047
|
-
const versionDir =
|
|
1048
|
-
await
|
|
1049
|
-
await
|
|
1047
|
+
const versionDir = join(state.input.directory, ".opencode");
|
|
1048
|
+
await mkdir(versionDir, { recursive: true });
|
|
1049
|
+
await writeFile(join(versionDir, HARNESS_VERSION_FILE), release.tagName + "\n", "utf-8");
|
|
1050
1050
|
// 8. Check if opencode.json changed
|
|
1051
1051
|
const hashAfter = await fileHash(configPath);
|
|
1052
1052
|
if (hashBefore && hashAfter && hashBefore !== hashAfter) {
|
|
@@ -1063,11 +1063,11 @@ async function syncHarness(state) {
|
|
|
1063
1063
|
finally {
|
|
1064
1064
|
// 9. Cleanup temp files
|
|
1065
1065
|
try {
|
|
1066
|
-
await
|
|
1066
|
+
await unlink(archivePath);
|
|
1067
1067
|
}
|
|
1068
1068
|
catch { /* ignore */ }
|
|
1069
1069
|
try {
|
|
1070
|
-
await
|
|
1070
|
+
await rm(extractDir, { recursive: true, force: true });
|
|
1071
1071
|
}
|
|
1072
1072
|
catch { /* ignore */ }
|
|
1073
1073
|
}
|
|
@@ -1598,22 +1598,22 @@ const ALL_CYCLES_COMPLETE_MARKER = "0-ULTRAWORK: ALL_CYCLES_COMPLETE";
|
|
|
1598
1598
|
* Skips silently if progress.md doesn't exist or is trivially empty.
|
|
1599
1599
|
*/
|
|
1600
1600
|
async function archiveProgress(directory) {
|
|
1601
|
-
const progressPath =
|
|
1601
|
+
const progressPath = join(directory, "memory-bank", "progress.md");
|
|
1602
1602
|
try {
|
|
1603
|
-
const content = await
|
|
1603
|
+
const content = await readFile(progressPath, "utf-8");
|
|
1604
1604
|
// Skip if empty or trivially empty
|
|
1605
1605
|
if (!content.trim() || content.trim() === "# Progress") {
|
|
1606
1606
|
pluginLog.info("[opencode-immune] Archive progress: nothing to archive (empty).");
|
|
1607
1607
|
return;
|
|
1608
1608
|
}
|
|
1609
|
-
const archiveDir =
|
|
1610
|
-
await
|
|
1609
|
+
const archiveDir = join(directory, "memory-bank", "archive");
|
|
1610
|
+
await mkdir(archiveDir, { recursive: true });
|
|
1611
1611
|
const now = new Date();
|
|
1612
1612
|
const dateStr = now.toISOString().slice(0, 10); // YYYY-MM-DD
|
|
1613
1613
|
const ts = Math.floor(now.getTime() / 1000);
|
|
1614
1614
|
const archiveName = `progress-${dateStr}-${ts}.md`;
|
|
1615
|
-
const archivePath =
|
|
1616
|
-
await
|
|
1615
|
+
const archivePath = join(archiveDir, archiveName);
|
|
1616
|
+
await rename(progressPath, archivePath);
|
|
1617
1617
|
pluginLog.info(`[opencode-immune] Archive progress: moved to ${archiveName}`);
|
|
1618
1618
|
}
|
|
1619
1619
|
catch (err) {
|
|
@@ -1664,17 +1664,17 @@ async function buildCommitMessage(directory, diffStat) {
|
|
|
1664
1664
|
function runGitCommit(directory) {
|
|
1665
1665
|
return new Promise((resolve) => {
|
|
1666
1666
|
// Stage all changes
|
|
1667
|
-
|
|
1667
|
+
execFile("git", ["add", "-A"], { cwd: directory }, (addErr) => {
|
|
1668
1668
|
if (addErr) {
|
|
1669
1669
|
pluginLog.error("[opencode-immune] git add failed:", addErr.message);
|
|
1670
1670
|
resolve(false);
|
|
1671
1671
|
return;
|
|
1672
1672
|
}
|
|
1673
1673
|
// Get diff stat for commit body
|
|
1674
|
-
|
|
1674
|
+
execFile("git", ["diff", "--cached", "--stat"], { cwd: directory }, async (_diffErr, diffOut) => {
|
|
1675
1675
|
const stat = (diffOut ?? "").trim();
|
|
1676
1676
|
const message = await buildCommitMessage(directory, stat);
|
|
1677
|
-
|
|
1677
|
+
execFile("git", ["commit", "-m", message], { cwd: directory }, (commitErr, stdout, stderr) => {
|
|
1678
1678
|
if (commitErr) {
|
|
1679
1679
|
if (stderr?.includes("nothing to commit") || stdout?.includes("nothing to commit")) {
|
|
1680
1680
|
pluginLog.info("[opencode-immune] git commit: nothing to commit (clean tree).");
|
|
@@ -1897,8 +1897,8 @@ async function server(input) {
|
|
|
1897
1897
|
else {
|
|
1898
1898
|
// No active task — check if backlog has pending work to start a new cycle
|
|
1899
1899
|
try {
|
|
1900
|
-
const backlogPath =
|
|
1901
|
-
const backlogContent = await
|
|
1900
|
+
const backlogPath = join(state.input.directory, "memory-bank", "backlog.md");
|
|
1901
|
+
const backlogContent = await readFile(backlogPath, "utf-8");
|
|
1902
1902
|
const hasPendingTasks = /- \[ \]/.test(backlogContent);
|
|
1903
1903
|
if (hasPendingTasks) {
|
|
1904
1904
|
state.autoResumeAttempted = true;
|
|
@@ -1960,7 +1960,7 @@ async function server(input) {
|
|
|
1960
1960
|
"permission.ask": withErrorBoundary(state, "permission.ask", createPermissionAskHandler(state)),
|
|
1961
1961
|
};
|
|
1962
1962
|
}
|
|
1963
|
-
|
|
1963
|
+
export default {
|
|
1964
1964
|
id: "opencode-immune",
|
|
1965
1965
|
server,
|
|
1966
1966
|
};
|
package/package.json
CHANGED