opencode-immune 1.0.48 → 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.
Files changed (2) hide show
  1. package/dist/plugin.js +119 -76
  2. 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
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const client_1 = require("@opencode-ai/sdk/v2/client");
7
- const promises_1 = require("fs/promises");
8
- const path_1 = require("path");
9
- const crypto_1 = require("crypto");
10
- const os_1 = require("os");
11
- const child_process_1 = require("child_process");
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.44";
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
- (0, path_1.join)(__dirname, "..", "package.json"),
28
- (0, path_1.join)(__dirname, "package.json"),
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 (0, promises_1.readFile)(pkgPath, "utf-8");
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: (0, client_1.createOpencodeClient)({
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: (0, path_1.join)(input.directory, ".opencode", "state", "ultrawork-active.json"),
91
- diagnosticsLogPath: (0, path_1.join)(input.directory, ".opencode", "state", "opencode-immune-debug.log"),
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 = (0, path_1.join)(state.input.directory, ".opencode", "state");
192
- await (0, promises_1.mkdir)(cacheDir, { recursive: true });
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 (0, promises_1.appendFile)(state.diagnosticsLogPath, `${line}\n`, "utf-8");
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 (0, promises_1.stat)(logPath);
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 (0, promises_1.rm)(rotatedPath, { force: true });
208
- await (0, promises_1.rename)(logPath, rotatedPath);
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 = (0, path_1.join)(directory, ".opencode", "state", "opencode-immune-debug.log");
231
+ const diagnosticsLogPath = join(directory, ".opencode", "state", "opencode-immune-debug.log");
232
232
  void (async () => {
233
233
  try {
234
- await (0, promises_1.mkdir)((0, path_1.dirname)(diagnosticsLogPath), { recursive: true });
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 (0, promises_1.appendFile)(diagnosticsLogPath, `${line}\n`, "utf-8");
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 = (0, path_1.join)(state.input.directory, ".opencode", "state");
267
- await (0, promises_1.mkdir)(dir, { recursive: true });
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 (0, promises_1.writeFile)(state.ultraworkMarkerPath, payload, "utf-8");
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 (0, promises_1.unlink)(state.ultraworkMarkerPath);
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 (0, promises_1.readFile)(state.ultraworkMarkerPath, "utf-8");
288
+ const raw = await readFile(state.ultraworkMarkerPath, "utf-8");
289
289
  const parsed = JSON.parse(raw);
290
290
  return parsed?.active === true;
291
291
  }
@@ -390,7 +390,7 @@ function isRetryableApiError(error) {
390
390
  return true;
391
391
  }
392
392
  // HTTP status code based detection (retryable server/gateway errors + rate limits)
393
- const status = maybeError.status ?? maybeError.statusCode ?? maybeError.data?.status;
393
+ const status = maybeError.status ?? maybeError.statusCode ?? maybeError.data?.status ?? maybeError.error?.status;
394
394
  if (status && (status === 404 || // transient endpoint not found (API gateway issues)
395
395
  status === 429 || // rate limit
396
396
  status === 500 || // internal server error
@@ -402,8 +402,18 @@ function isRetryableApiError(error) {
402
402
  }
403
403
  // Text-based detection for model access errors (not marked as retryable
404
404
  // by the API but retryable with a fallback model)
405
- const message = `${maybeError.message ?? ""} ${maybeError.data?.message ?? ""}`.toLowerCase();
406
- if (message.includes("не разрешен") ||
405
+ const message = [
406
+ maybeError.message,
407
+ maybeError.code,
408
+ maybeError.data?.message,
409
+ maybeError.data?.type,
410
+ maybeError.data?.code,
411
+ maybeError.error?.message,
412
+ maybeError.error?.type,
413
+ maybeError.error?.code,
414
+ ].filter((value) => value !== undefined && value !== null).join(" ").toLowerCase();
415
+ if (message.includes("api_error") ||
416
+ message.includes("не разрешен") ||
407
417
  message.includes("not allowed") ||
408
418
  message.includes("internal error") ||
409
419
  message.includes("internal server error") ||
@@ -439,6 +449,7 @@ function isRetryableApiError(error) {
439
449
  // Certificate/TLS provider failures must pass this primary retry gate
440
450
  // before the managed-session fallback model branch can run.
441
451
  message.includes("unknown certificate verification error") ||
452
+ message.includes("unknown_certificate_verification_error") ||
442
453
  message.includes("certificate has expired") ||
443
454
  message.includes("certificate verification") ||
444
455
  message.includes("tls") ||
@@ -468,23 +479,25 @@ function isRateLimitApiError(error) {
468
479
  if (!error || typeof error !== "object")
469
480
  return false;
470
481
  const maybeError = error;
471
- const message = `${maybeError.message ?? ""} ${maybeError.data?.message ?? ""}`.toLowerCase();
472
- const type = `${maybeError.data?.type ?? ""}`.toLowerCase();
482
+ const message = `${maybeError.message ?? ""} ${maybeError.data?.message ?? ""} ${maybeError.error?.message ?? ""}`.toLowerCase();
483
+ const type = `${maybeError.data?.type ?? ""} ${maybeError.error?.type ?? ""}`.toLowerCase();
473
484
  return ((type.includes("rate_limit") || message.includes("too many requests") || message.includes("rate limit")));
474
485
  }
475
486
  function isCertificateApiError(error) {
476
487
  if (!error || typeof error !== "object")
477
488
  return false;
478
489
  const maybeError = error;
479
- const message = `${maybeError.message ?? ""} ${maybeError.data?.message ?? ""}`.toLowerCase();
480
- const code = `${maybeError.code ?? ""} ${maybeError.data?.code ?? ""}`.toLowerCase();
481
- const type = `${maybeError.data?.type ?? ""}`.toLowerCase();
490
+ const message = `${maybeError.message ?? ""} ${maybeError.data?.message ?? ""} ${maybeError.error?.message ?? ""}`.toLowerCase();
491
+ const code = `${maybeError.code ?? ""} ${maybeError.data?.code ?? ""} ${maybeError.error?.code ?? ""}`.toLowerCase();
492
+ const type = `${maybeError.data?.type ?? ""} ${maybeError.error?.type ?? ""}`.toLowerCase();
482
493
  return (message.includes("unknown certificate verification error") ||
494
+ message.includes("unknown_certificate_verification_error") ||
483
495
  message.includes("certificate has expired") ||
484
496
  message.includes("certificate verification") ||
485
497
  message.includes("tls") ||
486
498
  message.includes("ssl") ||
487
499
  code.includes("cert_has_expired") ||
500
+ code.includes("unknown_certificate_verification_error") ||
488
501
  code.includes("unable_to_verify_leaf_signature") ||
489
502
  code.includes("self_signed_cert") ||
490
503
  code.includes("tls") ||
@@ -560,6 +573,28 @@ async function setSessionFallbackModel(state, sessionID, model) {
560
573
  fallbackModel: model,
561
574
  });
562
575
  }
576
+ async function recoverUntrackedRootSessionForRetry(state, sessionID, error) {
577
+ if (!isRetryableApiError(error))
578
+ return undefined;
579
+ const markerActive = await isUltraworkMarkerActive(state);
580
+ if (!markerActive)
581
+ return undefined;
582
+ const recovery = await parseTasksFile(state.input.directory);
583
+ if (!recovery || recovery.phase === "ARCHIVE: DONE")
584
+ return undefined;
585
+ state.recoveryContext = recovery;
586
+ await addManagedUltraworkSession(state, sessionID);
587
+ const recovered = getManagedSession(state, sessionID);
588
+ await writeDiagnosticLog(state, "session-retry:recovered-untracked-root", {
589
+ sessionID,
590
+ task: recovery.task,
591
+ level: recovery.level,
592
+ phase: recovery.phase,
593
+ errorType: getRetryableErrorType(error),
594
+ });
595
+ writePluginLog(state, "warn", `[opencode-immune] Recovered untracked root session ${sessionID} for retry after plugin restart or state loss.`);
596
+ return recovered;
597
+ }
563
598
  function getManagedSessionRetryContext(state, sessionID) {
564
599
  const managedSession = state.managedUltraworkSessions.get(sessionID);
565
600
  if (!managedSession)
@@ -707,8 +742,8 @@ function compositeChatMessage(handlers) {
707
742
  */
708
743
  async function parseTasksFile(directory) {
709
744
  try {
710
- const tasksPath = (0, path_1.join)(directory, "memory-bank", "tasks.md");
711
- const content = await (0, promises_1.readFile)(tasksPath, "utf-8");
745
+ const tasksPath = join(directory, "memory-bank", "tasks.md");
746
+ const content = await readFile(tasksPath, "utf-8");
712
747
  // Check for active task
713
748
  if (!content.includes("## Active Task") ||
714
749
  content.includes("No active tasks")) {
@@ -780,7 +815,7 @@ const DEFAULT_HARNESS_REPO = "gendoor/opencode-immune-harness";
780
815
  async function parseDotEnv(filePath) {
781
816
  const result = {};
782
817
  try {
783
- const content = await (0, promises_1.readFile)(filePath, "utf-8");
818
+ const content = await readFile(filePath, "utf-8");
784
819
  for (const line of content.split("\n")) {
785
820
  const trimmed = line.trim();
786
821
  if (!trimmed || trimmed.startsWith("#"))
@@ -814,13 +849,13 @@ async function resolveEnvValue(directory, key) {
814
849
  if (process.env[key])
815
850
  return process.env[key];
816
851
  // 2. Per-project .env
817
- const projectEnv = await parseDotEnv((0, path_1.join)(directory, ".env"));
852
+ const projectEnv = await parseDotEnv(join(directory, ".env"));
818
853
  if (projectEnv[key])
819
854
  return projectEnv[key];
820
855
  // 3. Global config
821
856
  const home = process.env.HOME || process.env.USERPROFILE || "";
822
857
  if (home) {
823
- const globalEnv = await parseDotEnv((0, path_1.join)(home, ".config", "opencode-immune", ".env"));
858
+ const globalEnv = await parseDotEnv(join(home, ".config", "opencode-immune", ".env"));
824
859
  if (globalEnv[key])
825
860
  return globalEnv[key];
826
861
  }
@@ -878,8 +913,8 @@ async function fetchLatestHarnessRelease(directory, repo, token) {
878
913
  */
879
914
  async function readLocalHarnessVersion(directory) {
880
915
  try {
881
- const versionPath = (0, path_1.join)(directory, ".opencode", HARNESS_VERSION_FILE);
882
- const content = await (0, promises_1.readFile)(versionPath, "utf-8");
916
+ const versionPath = join(directory, ".opencode", HARNESS_VERSION_FILE);
917
+ const content = await readFile(versionPath, "utf-8");
883
918
  return content.trim() || null;
884
919
  }
885
920
  catch {
@@ -903,8 +938,8 @@ async function downloadHarnessAsset(assetUrl, token) {
903
938
  throw new Error(`Download failed: ${resp.status} ${resp.statusText}`);
904
939
  }
905
940
  const buffer = Buffer.from(await resp.arrayBuffer());
906
- const tempPath = (0, path_1.join)((0, os_1.tmpdir)(), `harness-${Date.now()}.tar.gz`);
907
- await (0, promises_1.writeFile)(tempPath, buffer);
941
+ const tempPath = join(tmpdir(), `harness-${Date.now()}.tar.gz`);
942
+ await writeFile(tempPath, buffer);
908
943
  return tempPath;
909
944
  }
910
945
  /**
@@ -913,7 +948,7 @@ async function downloadHarnessAsset(assetUrl, token) {
913
948
  */
914
949
  function extractTarGz(archivePath, destDir) {
915
950
  return new Promise((resolve, reject) => {
916
- (0, child_process_1.execFile)("tar", ["xzf", archivePath, "-C", destDir], (err) => {
951
+ execFile("tar", ["xzf", archivePath, "-C", destDir], (err) => {
917
952
  if (err)
918
953
  reject(err);
919
954
  else
@@ -928,22 +963,22 @@ function extractTarGz(archivePath, destDir) {
928
963
  */
929
964
  async function copyDirRecursive(src, dest, skipRootFiles, rootDest) {
930
965
  const effectiveRoot = rootDest ?? dest;
931
- const entries = await (0, promises_1.readdir)(src, { withFileTypes: true });
932
- await (0, promises_1.mkdir)(dest, { recursive: true });
966
+ const entries = await readdir(src, { withFileTypes: true });
967
+ await mkdir(dest, { recursive: true });
933
968
  for (const entry of entries) {
934
969
  // Skip files only at the root destination level
935
970
  if (skipRootFiles && dest === effectiveRoot && entry.name === ".gitignore") {
936
971
  pluginLog.info(`[opencode-immune] Harness sync: skipping root .gitignore`);
937
972
  continue;
938
973
  }
939
- const srcPath = (0, path_1.join)(src, entry.name);
940
- const destPath = (0, path_1.join)(dest, entry.name);
974
+ const srcPath = join(src, entry.name);
975
+ const destPath = join(dest, entry.name);
941
976
  if (entry.isDirectory()) {
942
977
  await copyDirRecursive(srcPath, destPath, skipRootFiles, effectiveRoot);
943
978
  }
944
979
  else {
945
- await (0, promises_1.mkdir)((0, path_1.dirname)(destPath), { recursive: true });
946
- await (0, promises_1.copyFile)(srcPath, destPath);
980
+ await mkdir(dirname(destPath), { recursive: true });
981
+ await copyFile(srcPath, destPath);
947
982
  }
948
983
  }
949
984
  }
@@ -952,8 +987,8 @@ async function copyDirRecursive(src, dest, skipRootFiles, rootDest) {
952
987
  */
953
988
  async function fileHash(filePath) {
954
989
  try {
955
- const content = await (0, promises_1.readFile)(filePath);
956
- return (0, crypto_1.createHash)("sha256").update(content).digest("hex");
990
+ const content = await readFile(filePath);
991
+ return createHash("sha256").update(content).digest("hex");
957
992
  }
958
993
  catch {
959
994
  return "";
@@ -995,13 +1030,13 @@ async function syncHarness(state) {
995
1030
  }
996
1031
  pluginLog.info(`[opencode-immune] Harness sync: updating ${localVersion ?? "(none)"} → ${release.tagName}`);
997
1032
  // 3. Hash opencode.json before update
998
- const configPath = (0, path_1.join)(state.input.directory, "opencode.json");
1033
+ const configPath = join(state.input.directory, "opencode.json");
999
1034
  const hashBefore = await fileHash(configPath);
1000
1035
  // 4. Download asset
1001
1036
  const archivePath = await downloadHarnessAsset(release.assetUrl, token);
1002
1037
  // 5. Extract to temp dir
1003
- const extractDir = (0, path_1.join)((0, os_1.tmpdir)(), `harness-extract-${Date.now()}`);
1004
- await (0, promises_1.mkdir)(extractDir, { recursive: true });
1038
+ const extractDir = join(tmpdir(), `harness-extract-${Date.now()}`);
1039
+ await mkdir(extractDir, { recursive: true });
1005
1040
  try {
1006
1041
  await extractTarGz(archivePath, extractDir);
1007
1042
  // 6. Copy extracted files to project root (skip .gitignore — project owns its own)
@@ -1009,9 +1044,9 @@ async function syncHarness(state) {
1009
1044
  const SKIP_ROOT_FILES = new Set([".gitignore"]);
1010
1045
  await copyDirRecursive(extractDir, state.input.directory, SKIP_ROOT_FILES);
1011
1046
  // 7. Write version marker
1012
- const versionDir = (0, path_1.join)(state.input.directory, ".opencode");
1013
- await (0, promises_1.mkdir)(versionDir, { recursive: true });
1014
- await (0, promises_1.writeFile)((0, path_1.join)(versionDir, HARNESS_VERSION_FILE), release.tagName + "\n", "utf-8");
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");
1015
1050
  // 8. Check if opencode.json changed
1016
1051
  const hashAfter = await fileHash(configPath);
1017
1052
  if (hashBefore && hashAfter && hashBefore !== hashAfter) {
@@ -1028,11 +1063,11 @@ async function syncHarness(state) {
1028
1063
  finally {
1029
1064
  // 9. Cleanup temp files
1030
1065
  try {
1031
- await (0, promises_1.unlink)(archivePath);
1066
+ await unlink(archivePath);
1032
1067
  }
1033
1068
  catch { /* ignore */ }
1034
1069
  try {
1035
- await (0, promises_1.rm)(extractDir, { recursive: true, force: true });
1070
+ await rm(extractDir, { recursive: true, force: true });
1036
1071
  }
1037
1072
  catch { /* ignore */ }
1038
1073
  }
@@ -1459,10 +1494,18 @@ function createEventHandler(state) {
1459
1494
  }
1460
1495
  // ── Auto-retry on retryable API error for managed ultrawork sessions ──
1461
1496
  if (eventType === "session.error" && sessionID) {
1462
- const managedSession = getManagedSession(state, sessionID);
1497
+ const managedSession = getManagedSession(state, sessionID) ??
1498
+ await recoverUntrackedRootSessionForRetry(state, sessionID, error);
1463
1499
  const isRoot = managedSession?.kind === "root";
1464
1500
  const isChild = managedSession?.kind === "child";
1465
1501
  if (!managedSession) {
1502
+ if (isRetryableApiError(error)) {
1503
+ await writeDiagnosticLog(state, "session-retry:unmanaged-retryable-error", {
1504
+ sessionID,
1505
+ eventType,
1506
+ errorType: getRetryableErrorType(error),
1507
+ });
1508
+ }
1466
1509
  return;
1467
1510
  }
1468
1511
  if (!isRetryableApiError(error)) {
@@ -1555,22 +1598,22 @@ const ALL_CYCLES_COMPLETE_MARKER = "0-ULTRAWORK: ALL_CYCLES_COMPLETE";
1555
1598
  * Skips silently if progress.md doesn't exist or is trivially empty.
1556
1599
  */
1557
1600
  async function archiveProgress(directory) {
1558
- const progressPath = (0, path_1.join)(directory, "memory-bank", "progress.md");
1601
+ const progressPath = join(directory, "memory-bank", "progress.md");
1559
1602
  try {
1560
- const content = await (0, promises_1.readFile)(progressPath, "utf-8");
1603
+ const content = await readFile(progressPath, "utf-8");
1561
1604
  // Skip if empty or trivially empty
1562
1605
  if (!content.trim() || content.trim() === "# Progress") {
1563
1606
  pluginLog.info("[opencode-immune] Archive progress: nothing to archive (empty).");
1564
1607
  return;
1565
1608
  }
1566
- const archiveDir = (0, path_1.join)(directory, "memory-bank", "archive");
1567
- await (0, promises_1.mkdir)(archiveDir, { recursive: true });
1609
+ const archiveDir = join(directory, "memory-bank", "archive");
1610
+ await mkdir(archiveDir, { recursive: true });
1568
1611
  const now = new Date();
1569
1612
  const dateStr = now.toISOString().slice(0, 10); // YYYY-MM-DD
1570
1613
  const ts = Math.floor(now.getTime() / 1000);
1571
1614
  const archiveName = `progress-${dateStr}-${ts}.md`;
1572
- const archivePath = (0, path_1.join)(archiveDir, archiveName);
1573
- await (0, promises_1.rename)(progressPath, archivePath);
1615
+ const archivePath = join(archiveDir, archiveName);
1616
+ await rename(progressPath, archivePath);
1574
1617
  pluginLog.info(`[opencode-immune] Archive progress: moved to ${archiveName}`);
1575
1618
  }
1576
1619
  catch (err) {
@@ -1621,17 +1664,17 @@ async function buildCommitMessage(directory, diffStat) {
1621
1664
  function runGitCommit(directory) {
1622
1665
  return new Promise((resolve) => {
1623
1666
  // Stage all changes
1624
- (0, child_process_1.execFile)("git", ["add", "-A"], { cwd: directory }, (addErr) => {
1667
+ execFile("git", ["add", "-A"], { cwd: directory }, (addErr) => {
1625
1668
  if (addErr) {
1626
1669
  pluginLog.error("[opencode-immune] git add failed:", addErr.message);
1627
1670
  resolve(false);
1628
1671
  return;
1629
1672
  }
1630
1673
  // Get diff stat for commit body
1631
- (0, child_process_1.execFile)("git", ["diff", "--cached", "--stat"], { cwd: directory }, async (_diffErr, diffOut) => {
1674
+ execFile("git", ["diff", "--cached", "--stat"], { cwd: directory }, async (_diffErr, diffOut) => {
1632
1675
  const stat = (diffOut ?? "").trim();
1633
1676
  const message = await buildCommitMessage(directory, stat);
1634
- (0, child_process_1.execFile)("git", ["commit", "-m", message], { cwd: directory }, (commitErr, stdout, stderr) => {
1677
+ execFile("git", ["commit", "-m", message], { cwd: directory }, (commitErr, stdout, stderr) => {
1635
1678
  if (commitErr) {
1636
1679
  if (stderr?.includes("nothing to commit") || stdout?.includes("nothing to commit")) {
1637
1680
  pluginLog.info("[opencode-immune] git commit: nothing to commit (clean tree).");
@@ -1854,8 +1897,8 @@ async function server(input) {
1854
1897
  else {
1855
1898
  // No active task — check if backlog has pending work to start a new cycle
1856
1899
  try {
1857
- const backlogPath = (0, path_1.join)(state.input.directory, "memory-bank", "backlog.md");
1858
- const backlogContent = await (0, promises_1.readFile)(backlogPath, "utf-8");
1900
+ const backlogPath = join(state.input.directory, "memory-bank", "backlog.md");
1901
+ const backlogContent = await readFile(backlogPath, "utf-8");
1859
1902
  const hasPendingTasks = /- \[ \]/.test(backlogContent);
1860
1903
  if (hasPendingTasks) {
1861
1904
  state.autoResumeAttempted = true;
@@ -1917,7 +1960,7 @@ async function server(input) {
1917
1960
  "permission.ask": withErrorBoundary(state, "permission.ask", createPermissionAskHandler(state)),
1918
1961
  };
1919
1962
  }
1920
- exports.default = {
1963
+ export default {
1921
1964
  id: "opencode-immune",
1922
1965
  server,
1923
1966
  };
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "opencode-immune",
3
- "version": "1.0.48",
3
+ "version": "1.0.51",
4
+ "type": "module",
4
5
  "description": "OpenCode plugin: session recovery, auto-retry, multi-cycle automation, context monitoring",
5
6
  "exports": {
6
7
  "./server": "./dist/plugin.js"