opencode-immune 1.0.49 → 1.0.52
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 +92 -70
- 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.52";
|
|
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,
|
|
@@ -122,6 +122,7 @@ const DIAGNOSTIC_LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
|
122
122
|
let activeLogDirectory = null;
|
|
123
123
|
const MANAGED_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
124
124
|
const PROVIDER_RETRY_WATCHDOG_MS = 30_000;
|
|
125
|
+
const RETRY_PROMPT_DELIVERY_ATTEMPTS = 3;
|
|
125
126
|
const CHILD_FALLBACK_REQUEST_TTL_MS = 10 * 60 * 1000;
|
|
126
127
|
const RATE_LIMIT_FALLBACK_MODEL = {
|
|
127
128
|
providerID: "codexsale",
|
|
@@ -154,7 +155,7 @@ async function createManagedUltraworkSession(state, title) {
|
|
|
154
155
|
return sessionID;
|
|
155
156
|
}
|
|
156
157
|
async function promptManagedSession(state, sessionID, text, options = {}) {
|
|
157
|
-
await state.client.session.promptAsync({
|
|
158
|
+
const result = await state.client.session.promptAsync({
|
|
158
159
|
directory: state.input.directory,
|
|
159
160
|
sessionID,
|
|
160
161
|
...(options.model ? { model: options.model } : {}),
|
|
@@ -166,6 +167,9 @@ async function promptManagedSession(state, sessionID, text, options = {}) {
|
|
|
166
167
|
},
|
|
167
168
|
],
|
|
168
169
|
});
|
|
170
|
+
if (result.error || !result.response.ok) {
|
|
171
|
+
throw new Error(`prompt_async failed with status ${result.response.status}: ${JSON.stringify(result.error ?? null)}`);
|
|
172
|
+
}
|
|
169
173
|
}
|
|
170
174
|
async function abortManagedSession(state, sessionID) {
|
|
171
175
|
await state.client.session.abort({
|
|
@@ -188,11 +192,11 @@ function pruneExpiredManagedSessions(state, now = Date.now()) {
|
|
|
188
192
|
}
|
|
189
193
|
async function writeDiagnosticLog(state, event, data = {}) {
|
|
190
194
|
try {
|
|
191
|
-
const cacheDir =
|
|
192
|
-
await
|
|
195
|
+
const cacheDir = join(state.input.directory, ".opencode", "state");
|
|
196
|
+
await mkdir(cacheDir, { recursive: true });
|
|
193
197
|
await rotateDiagnosticLogIfNeeded(state.diagnosticsLogPath);
|
|
194
198
|
const line = JSON.stringify({ ts: new Date().toISOString(), event, ...data });
|
|
195
|
-
await
|
|
199
|
+
await appendFile(state.diagnosticsLogPath, `${line}\n`, "utf-8");
|
|
196
200
|
}
|
|
197
201
|
catch {
|
|
198
202
|
// diagnostics must never affect runtime behavior
|
|
@@ -200,12 +204,12 @@ async function writeDiagnosticLog(state, event, data = {}) {
|
|
|
200
204
|
}
|
|
201
205
|
async function rotateDiagnosticLogIfNeeded(logPath) {
|
|
202
206
|
try {
|
|
203
|
-
const current = await
|
|
207
|
+
const current = await stat(logPath);
|
|
204
208
|
if (current.size < DIAGNOSTIC_LOG_MAX_BYTES)
|
|
205
209
|
return;
|
|
206
210
|
const rotatedPath = `${logPath}.1`;
|
|
207
|
-
await
|
|
208
|
-
await
|
|
211
|
+
await rm(rotatedPath, { force: true });
|
|
212
|
+
await rename(logPath, rotatedPath);
|
|
209
213
|
}
|
|
210
214
|
catch {
|
|
211
215
|
// missing log or rotation failure must never affect runtime behavior
|
|
@@ -228,10 +232,10 @@ function writePluginLog(state, level, message, extra = {}) {
|
|
|
228
232
|
});
|
|
229
233
|
}
|
|
230
234
|
function writePluginLogForDirectory(directory, level, message, extra = {}) {
|
|
231
|
-
const diagnosticsLogPath =
|
|
235
|
+
const diagnosticsLogPath = join(directory, ".opencode", "state", "opencode-immune-debug.log");
|
|
232
236
|
void (async () => {
|
|
233
237
|
try {
|
|
234
|
-
await
|
|
238
|
+
await mkdir(dirname(diagnosticsLogPath), { recursive: true });
|
|
235
239
|
await rotateDiagnosticLogIfNeeded(diagnosticsLogPath);
|
|
236
240
|
const line = JSON.stringify({
|
|
237
241
|
ts: new Date().toISOString(),
|
|
@@ -239,7 +243,7 @@ function writePluginLogForDirectory(directory, level, message, extra = {}) {
|
|
|
239
243
|
message,
|
|
240
244
|
...extra,
|
|
241
245
|
});
|
|
242
|
-
await
|
|
246
|
+
await appendFile(diagnosticsLogPath, `${line}\n`, "utf-8");
|
|
243
247
|
}
|
|
244
248
|
catch {
|
|
245
249
|
// file logging must never affect runtime behavior
|
|
@@ -263,13 +267,13 @@ const pluginLog = {
|
|
|
263
267
|
// ── Ultrawork Marker File ──
|
|
264
268
|
async function writeUltraworkMarker(state) {
|
|
265
269
|
try {
|
|
266
|
-
const dir =
|
|
267
|
-
await
|
|
270
|
+
const dir = join(state.input.directory, ".opencode", "state");
|
|
271
|
+
await mkdir(dir, { recursive: true });
|
|
268
272
|
const payload = JSON.stringify({
|
|
269
273
|
active: true,
|
|
270
274
|
updatedAt: new Date().toISOString(),
|
|
271
275
|
});
|
|
272
|
-
await
|
|
276
|
+
await writeFile(state.ultraworkMarkerPath, payload, "utf-8");
|
|
273
277
|
}
|
|
274
278
|
catch {
|
|
275
279
|
// marker write must never affect runtime
|
|
@@ -277,7 +281,7 @@ async function writeUltraworkMarker(state) {
|
|
|
277
281
|
}
|
|
278
282
|
async function clearUltraworkMarker(state) {
|
|
279
283
|
try {
|
|
280
|
-
await
|
|
284
|
+
await unlink(state.ultraworkMarkerPath);
|
|
281
285
|
}
|
|
282
286
|
catch {
|
|
283
287
|
// file may not exist — that's fine
|
|
@@ -285,7 +289,7 @@ async function clearUltraworkMarker(state) {
|
|
|
285
289
|
}
|
|
286
290
|
async function isUltraworkMarkerActive(state) {
|
|
287
291
|
try {
|
|
288
|
-
const raw = await
|
|
292
|
+
const raw = await readFile(state.ultraworkMarkerPath, "utf-8");
|
|
289
293
|
const parsed = JSON.parse(raw);
|
|
290
294
|
return parsed?.active === true;
|
|
291
295
|
}
|
|
@@ -673,12 +677,30 @@ function scheduleManagedSessionRetry(state, sessionID, options) {
|
|
|
673
677
|
abortBeforePrompt: options.abortBeforePrompt,
|
|
674
678
|
});
|
|
675
679
|
}
|
|
676
|
-
catch {
|
|
680
|
+
catch (err) {
|
|
677
681
|
if (options.countAgainstBudget) {
|
|
678
682
|
state.sessionErrorRetryCount.set(sessionID, Math.max((state.sessionErrorRetryCount.get(sessionID) ?? 1) - 1, 0));
|
|
679
683
|
}
|
|
684
|
+
const deliveryAttempt = options.deliveryAttempt ?? 1;
|
|
685
|
+
await writeDiagnosticLog(state, "session-retry:prompt-delivery-failed", {
|
|
686
|
+
sessionID,
|
|
687
|
+
reason: options.reason,
|
|
688
|
+
deliveryAttempt,
|
|
689
|
+
error: err instanceof Error ? err.message : String(err),
|
|
690
|
+
});
|
|
680
691
|
writePluginLog(state, "warn", `[opencode-immune] Retry prompt failed for session ${sessionID}. ` +
|
|
681
|
-
|
|
692
|
+
(deliveryAttempt < RETRY_PROMPT_DELIVERY_ATTEMPTS
|
|
693
|
+
? `Retrying delivery attempt ${deliveryAttempt + 1}/${RETRY_PROMPT_DELIVERY_ATTEMPTS}.`
|
|
694
|
+
: `Delivery attempts exhausted; will wait for the next retry signal.`));
|
|
695
|
+
if (deliveryAttempt < RETRY_PROMPT_DELIVERY_ATTEMPTS) {
|
|
696
|
+
scheduleManagedSessionRetry(state, sessionID, {
|
|
697
|
+
...options,
|
|
698
|
+
delayMs: Math.min(options.delayMs * 2, 15_000),
|
|
699
|
+
countAgainstBudget: false,
|
|
700
|
+
abortBeforePrompt: false,
|
|
701
|
+
deliveryAttempt: deliveryAttempt + 1,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
682
704
|
}
|
|
683
705
|
}, options.delayMs);
|
|
684
706
|
state.sessionRetryTimers.set(sessionID, timer);
|
|
@@ -742,8 +764,8 @@ function compositeChatMessage(handlers) {
|
|
|
742
764
|
*/
|
|
743
765
|
async function parseTasksFile(directory) {
|
|
744
766
|
try {
|
|
745
|
-
const tasksPath =
|
|
746
|
-
const content = await
|
|
767
|
+
const tasksPath = join(directory, "memory-bank", "tasks.md");
|
|
768
|
+
const content = await readFile(tasksPath, "utf-8");
|
|
747
769
|
// Check for active task
|
|
748
770
|
if (!content.includes("## Active Task") ||
|
|
749
771
|
content.includes("No active tasks")) {
|
|
@@ -815,7 +837,7 @@ const DEFAULT_HARNESS_REPO = "gendoor/opencode-immune-harness";
|
|
|
815
837
|
async function parseDotEnv(filePath) {
|
|
816
838
|
const result = {};
|
|
817
839
|
try {
|
|
818
|
-
const content = await
|
|
840
|
+
const content = await readFile(filePath, "utf-8");
|
|
819
841
|
for (const line of content.split("\n")) {
|
|
820
842
|
const trimmed = line.trim();
|
|
821
843
|
if (!trimmed || trimmed.startsWith("#"))
|
|
@@ -849,13 +871,13 @@ async function resolveEnvValue(directory, key) {
|
|
|
849
871
|
if (process.env[key])
|
|
850
872
|
return process.env[key];
|
|
851
873
|
// 2. Per-project .env
|
|
852
|
-
const projectEnv = await parseDotEnv(
|
|
874
|
+
const projectEnv = await parseDotEnv(join(directory, ".env"));
|
|
853
875
|
if (projectEnv[key])
|
|
854
876
|
return projectEnv[key];
|
|
855
877
|
// 3. Global config
|
|
856
878
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
857
879
|
if (home) {
|
|
858
|
-
const globalEnv = await parseDotEnv(
|
|
880
|
+
const globalEnv = await parseDotEnv(join(home, ".config", "opencode-immune", ".env"));
|
|
859
881
|
if (globalEnv[key])
|
|
860
882
|
return globalEnv[key];
|
|
861
883
|
}
|
|
@@ -913,8 +935,8 @@ async function fetchLatestHarnessRelease(directory, repo, token) {
|
|
|
913
935
|
*/
|
|
914
936
|
async function readLocalHarnessVersion(directory) {
|
|
915
937
|
try {
|
|
916
|
-
const versionPath =
|
|
917
|
-
const content = await
|
|
938
|
+
const versionPath = join(directory, ".opencode", HARNESS_VERSION_FILE);
|
|
939
|
+
const content = await readFile(versionPath, "utf-8");
|
|
918
940
|
return content.trim() || null;
|
|
919
941
|
}
|
|
920
942
|
catch {
|
|
@@ -938,8 +960,8 @@ async function downloadHarnessAsset(assetUrl, token) {
|
|
|
938
960
|
throw new Error(`Download failed: ${resp.status} ${resp.statusText}`);
|
|
939
961
|
}
|
|
940
962
|
const buffer = Buffer.from(await resp.arrayBuffer());
|
|
941
|
-
const tempPath =
|
|
942
|
-
await
|
|
963
|
+
const tempPath = join(tmpdir(), `harness-${Date.now()}.tar.gz`);
|
|
964
|
+
await writeFile(tempPath, buffer);
|
|
943
965
|
return tempPath;
|
|
944
966
|
}
|
|
945
967
|
/**
|
|
@@ -948,7 +970,7 @@ async function downloadHarnessAsset(assetUrl, token) {
|
|
|
948
970
|
*/
|
|
949
971
|
function extractTarGz(archivePath, destDir) {
|
|
950
972
|
return new Promise((resolve, reject) => {
|
|
951
|
-
|
|
973
|
+
execFile("tar", ["xzf", archivePath, "-C", destDir], (err) => {
|
|
952
974
|
if (err)
|
|
953
975
|
reject(err);
|
|
954
976
|
else
|
|
@@ -963,22 +985,22 @@ function extractTarGz(archivePath, destDir) {
|
|
|
963
985
|
*/
|
|
964
986
|
async function copyDirRecursive(src, dest, skipRootFiles, rootDest) {
|
|
965
987
|
const effectiveRoot = rootDest ?? dest;
|
|
966
|
-
const entries = await
|
|
967
|
-
await
|
|
988
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
989
|
+
await mkdir(dest, { recursive: true });
|
|
968
990
|
for (const entry of entries) {
|
|
969
991
|
// Skip files only at the root destination level
|
|
970
992
|
if (skipRootFiles && dest === effectiveRoot && entry.name === ".gitignore") {
|
|
971
993
|
pluginLog.info(`[opencode-immune] Harness sync: skipping root .gitignore`);
|
|
972
994
|
continue;
|
|
973
995
|
}
|
|
974
|
-
const srcPath =
|
|
975
|
-
const destPath =
|
|
996
|
+
const srcPath = join(src, entry.name);
|
|
997
|
+
const destPath = join(dest, entry.name);
|
|
976
998
|
if (entry.isDirectory()) {
|
|
977
999
|
await copyDirRecursive(srcPath, destPath, skipRootFiles, effectiveRoot);
|
|
978
1000
|
}
|
|
979
1001
|
else {
|
|
980
|
-
await
|
|
981
|
-
await
|
|
1002
|
+
await mkdir(dirname(destPath), { recursive: true });
|
|
1003
|
+
await copyFile(srcPath, destPath);
|
|
982
1004
|
}
|
|
983
1005
|
}
|
|
984
1006
|
}
|
|
@@ -987,8 +1009,8 @@ async function copyDirRecursive(src, dest, skipRootFiles, rootDest) {
|
|
|
987
1009
|
*/
|
|
988
1010
|
async function fileHash(filePath) {
|
|
989
1011
|
try {
|
|
990
|
-
const content = await
|
|
991
|
-
return
|
|
1012
|
+
const content = await readFile(filePath);
|
|
1013
|
+
return createHash("sha256").update(content).digest("hex");
|
|
992
1014
|
}
|
|
993
1015
|
catch {
|
|
994
1016
|
return "";
|
|
@@ -1030,13 +1052,13 @@ async function syncHarness(state) {
|
|
|
1030
1052
|
}
|
|
1031
1053
|
pluginLog.info(`[opencode-immune] Harness sync: updating ${localVersion ?? "(none)"} → ${release.tagName}`);
|
|
1032
1054
|
// 3. Hash opencode.json before update
|
|
1033
|
-
const configPath =
|
|
1055
|
+
const configPath = join(state.input.directory, "opencode.json");
|
|
1034
1056
|
const hashBefore = await fileHash(configPath);
|
|
1035
1057
|
// 4. Download asset
|
|
1036
1058
|
const archivePath = await downloadHarnessAsset(release.assetUrl, token);
|
|
1037
1059
|
// 5. Extract to temp dir
|
|
1038
|
-
const extractDir =
|
|
1039
|
-
await
|
|
1060
|
+
const extractDir = join(tmpdir(), `harness-extract-${Date.now()}`);
|
|
1061
|
+
await mkdir(extractDir, { recursive: true });
|
|
1040
1062
|
try {
|
|
1041
1063
|
await extractTarGz(archivePath, extractDir);
|
|
1042
1064
|
// 6. Copy extracted files to project root (skip .gitignore — project owns its own)
|
|
@@ -1044,9 +1066,9 @@ async function syncHarness(state) {
|
|
|
1044
1066
|
const SKIP_ROOT_FILES = new Set([".gitignore"]);
|
|
1045
1067
|
await copyDirRecursive(extractDir, state.input.directory, SKIP_ROOT_FILES);
|
|
1046
1068
|
// 7. Write version marker
|
|
1047
|
-
const versionDir =
|
|
1048
|
-
await
|
|
1049
|
-
await
|
|
1069
|
+
const versionDir = join(state.input.directory, ".opencode");
|
|
1070
|
+
await mkdir(versionDir, { recursive: true });
|
|
1071
|
+
await writeFile(join(versionDir, HARNESS_VERSION_FILE), release.tagName + "\n", "utf-8");
|
|
1050
1072
|
// 8. Check if opencode.json changed
|
|
1051
1073
|
const hashAfter = await fileHash(configPath);
|
|
1052
1074
|
if (hashBefore && hashAfter && hashBefore !== hashAfter) {
|
|
@@ -1063,11 +1085,11 @@ async function syncHarness(state) {
|
|
|
1063
1085
|
finally {
|
|
1064
1086
|
// 9. Cleanup temp files
|
|
1065
1087
|
try {
|
|
1066
|
-
await
|
|
1088
|
+
await unlink(archivePath);
|
|
1067
1089
|
}
|
|
1068
1090
|
catch { /* ignore */ }
|
|
1069
1091
|
try {
|
|
1070
|
-
await
|
|
1092
|
+
await rm(extractDir, { recursive: true, force: true });
|
|
1071
1093
|
}
|
|
1072
1094
|
catch { /* ignore */ }
|
|
1073
1095
|
}
|
|
@@ -1598,22 +1620,22 @@ const ALL_CYCLES_COMPLETE_MARKER = "0-ULTRAWORK: ALL_CYCLES_COMPLETE";
|
|
|
1598
1620
|
* Skips silently if progress.md doesn't exist or is trivially empty.
|
|
1599
1621
|
*/
|
|
1600
1622
|
async function archiveProgress(directory) {
|
|
1601
|
-
const progressPath =
|
|
1623
|
+
const progressPath = join(directory, "memory-bank", "progress.md");
|
|
1602
1624
|
try {
|
|
1603
|
-
const content = await
|
|
1625
|
+
const content = await readFile(progressPath, "utf-8");
|
|
1604
1626
|
// Skip if empty or trivially empty
|
|
1605
1627
|
if (!content.trim() || content.trim() === "# Progress") {
|
|
1606
1628
|
pluginLog.info("[opencode-immune] Archive progress: nothing to archive (empty).");
|
|
1607
1629
|
return;
|
|
1608
1630
|
}
|
|
1609
|
-
const archiveDir =
|
|
1610
|
-
await
|
|
1631
|
+
const archiveDir = join(directory, "memory-bank", "archive");
|
|
1632
|
+
await mkdir(archiveDir, { recursive: true });
|
|
1611
1633
|
const now = new Date();
|
|
1612
1634
|
const dateStr = now.toISOString().slice(0, 10); // YYYY-MM-DD
|
|
1613
1635
|
const ts = Math.floor(now.getTime() / 1000);
|
|
1614
1636
|
const archiveName = `progress-${dateStr}-${ts}.md`;
|
|
1615
|
-
const archivePath =
|
|
1616
|
-
await
|
|
1637
|
+
const archivePath = join(archiveDir, archiveName);
|
|
1638
|
+
await rename(progressPath, archivePath);
|
|
1617
1639
|
pluginLog.info(`[opencode-immune] Archive progress: moved to ${archiveName}`);
|
|
1618
1640
|
}
|
|
1619
1641
|
catch (err) {
|
|
@@ -1664,17 +1686,17 @@ async function buildCommitMessage(directory, diffStat) {
|
|
|
1664
1686
|
function runGitCommit(directory) {
|
|
1665
1687
|
return new Promise((resolve) => {
|
|
1666
1688
|
// Stage all changes
|
|
1667
|
-
|
|
1689
|
+
execFile("git", ["add", "-A"], { cwd: directory }, (addErr) => {
|
|
1668
1690
|
if (addErr) {
|
|
1669
1691
|
pluginLog.error("[opencode-immune] git add failed:", addErr.message);
|
|
1670
1692
|
resolve(false);
|
|
1671
1693
|
return;
|
|
1672
1694
|
}
|
|
1673
1695
|
// Get diff stat for commit body
|
|
1674
|
-
|
|
1696
|
+
execFile("git", ["diff", "--cached", "--stat"], { cwd: directory }, async (_diffErr, diffOut) => {
|
|
1675
1697
|
const stat = (diffOut ?? "").trim();
|
|
1676
1698
|
const message = await buildCommitMessage(directory, stat);
|
|
1677
|
-
|
|
1699
|
+
execFile("git", ["commit", "-m", message], { cwd: directory }, (commitErr, stdout, stderr) => {
|
|
1678
1700
|
if (commitErr) {
|
|
1679
1701
|
if (stderr?.includes("nothing to commit") || stdout?.includes("nothing to commit")) {
|
|
1680
1702
|
pluginLog.info("[opencode-immune] git commit: nothing to commit (clean tree).");
|
|
@@ -1897,8 +1919,8 @@ async function server(input) {
|
|
|
1897
1919
|
else {
|
|
1898
1920
|
// No active task — check if backlog has pending work to start a new cycle
|
|
1899
1921
|
try {
|
|
1900
|
-
const backlogPath =
|
|
1901
|
-
const backlogContent = await
|
|
1922
|
+
const backlogPath = join(state.input.directory, "memory-bank", "backlog.md");
|
|
1923
|
+
const backlogContent = await readFile(backlogPath, "utf-8");
|
|
1902
1924
|
const hasPendingTasks = /- \[ \]/.test(backlogContent);
|
|
1903
1925
|
if (hasPendingTasks) {
|
|
1904
1926
|
state.autoResumeAttempted = true;
|
|
@@ -1960,7 +1982,7 @@ async function server(input) {
|
|
|
1960
1982
|
"permission.ask": withErrorBoundary(state, "permission.ask", createPermissionAskHandler(state)),
|
|
1961
1983
|
};
|
|
1962
1984
|
}
|
|
1963
|
-
|
|
1985
|
+
export default {
|
|
1964
1986
|
id: "opencode-immune",
|
|
1965
1987
|
server,
|
|
1966
1988
|
};
|
package/package.json
CHANGED