opencode-immune 1.0.45 → 1.0.46

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 +161 -91
  2. package/package.json +1 -1
package/dist/plugin.js CHANGED
@@ -59,10 +59,10 @@ async function checkPluginUpdate(state) {
59
59
  state.pluginUpdateMessage =
60
60
  `[PLUGIN UPDATE] opencode-immune ${currentVersion} → ${latest} is available. ` +
61
61
  `Please inform the user: a plugin update is available. They should restart opencode to get the latest version.`;
62
- console.warn(`[opencode-immune] Plugin update available: ${currentVersion} → ${latest}.`);
62
+ writePluginLog(state, "warn", `[opencode-immune] Plugin update available: ${currentVersion} → ${latest}.`);
63
63
  }
64
64
  else if (latest) {
65
- console.log(`[opencode-immune] Plugin version ${currentVersion} is up to date.`);
65
+ writePluginLog(state, "info", `[opencode-immune] Plugin version ${currentVersion} is up to date.`);
66
66
  }
67
67
  }
68
68
  catch {
@@ -70,6 +70,7 @@ async function checkPluginUpdate(state) {
70
70
  }
71
71
  }
72
72
  function createState(input) {
73
+ activeLogDirectory = input.directory;
73
74
  return {
74
75
  input,
75
76
  recoveryContext: null,
@@ -92,6 +93,8 @@ function createState(input) {
92
93
  };
93
94
  }
94
95
  const ULTRAWORK_AGENT = "0-ultrawork";
96
+ const DIAGNOSTIC_LOG_MAX_BYTES = 5 * 1024 * 1024;
97
+ let activeLogDirectory = null;
95
98
  const MANAGED_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
96
99
  const PROVIDER_RETRY_WATCHDOG_MS = 30_000;
97
100
  const CHILD_FALLBACK_REQUEST_TTL_MS = 10 * 60 * 1000;
@@ -130,6 +133,7 @@ async function writeDiagnosticLog(state, event, data = {}) {
130
133
  try {
131
134
  const cacheDir = (0, path_1.join)(state.input.directory, ".opencode", "state");
132
135
  await (0, promises_1.mkdir)(cacheDir, { recursive: true });
136
+ await rotateDiagnosticLogIfNeeded(state.diagnosticsLogPath);
133
137
  const line = JSON.stringify({ ts: new Date().toISOString(), event, ...data });
134
138
  await (0, promises_1.appendFile)(state.diagnosticsLogPath, `${line}\n`, "utf-8");
135
139
  }
@@ -137,6 +141,68 @@ async function writeDiagnosticLog(state, event, data = {}) {
137
141
  // diagnostics must never affect runtime behavior
138
142
  }
139
143
  }
144
+ async function rotateDiagnosticLogIfNeeded(logPath) {
145
+ try {
146
+ const current = await (0, promises_1.stat)(logPath);
147
+ if (current.size < DIAGNOSTIC_LOG_MAX_BYTES)
148
+ return;
149
+ const rotatedPath = `${logPath}.1`;
150
+ await (0, promises_1.rm)(rotatedPath, { force: true });
151
+ await (0, promises_1.rename)(logPath, rotatedPath);
152
+ }
153
+ catch {
154
+ // missing log or rotation failure must never affect runtime behavior
155
+ }
156
+ }
157
+ function normalizeLogValue(value) {
158
+ if (value instanceof Error) {
159
+ return {
160
+ name: value.name,
161
+ message: value.message,
162
+ stack: value.stack,
163
+ };
164
+ }
165
+ return value;
166
+ }
167
+ function writePluginLog(state, level, message, extra = {}) {
168
+ void writeDiagnosticLog(state, `log:${level}`, {
169
+ message,
170
+ ...extra,
171
+ });
172
+ }
173
+ function writePluginLogForDirectory(directory, level, message, extra = {}) {
174
+ const diagnosticsLogPath = (0, path_1.join)(directory, ".opencode", "state", "opencode-immune-debug.log");
175
+ void (async () => {
176
+ try {
177
+ await (0, promises_1.mkdir)((0, path_1.dirname)(diagnosticsLogPath), { recursive: true });
178
+ await rotateDiagnosticLogIfNeeded(diagnosticsLogPath);
179
+ const line = JSON.stringify({
180
+ ts: new Date().toISOString(),
181
+ event: `log:${level}`,
182
+ message,
183
+ ...extra,
184
+ });
185
+ await (0, promises_1.appendFile)(diagnosticsLogPath, `${line}\n`, "utf-8");
186
+ }
187
+ catch {
188
+ // file logging must never affect runtime behavior
189
+ }
190
+ })();
191
+ }
192
+ function writePluginLogFromArgs(level, values) {
193
+ if (!activeLogDirectory)
194
+ return;
195
+ const [first, ...rest] = values;
196
+ const message = typeof first === "string" ? first : JSON.stringify(normalizeLogValue(first));
197
+ writePluginLogForDirectory(activeLogDirectory, level, message, {
198
+ args: rest.map(normalizeLogValue),
199
+ });
200
+ }
201
+ const pluginLog = {
202
+ info: (...values) => writePluginLogFromArgs("info", values),
203
+ warn: (...values) => writePluginLogFromArgs("warn", values),
204
+ error: (...values) => writePluginLogFromArgs("error", values),
205
+ };
140
206
  // ── Ultrawork Marker File ──
141
207
  async function writeUltraworkMarker(state) {
142
208
  try {
@@ -214,7 +280,7 @@ function cancelPendingSessionRetry(state, sessionID, reason) {
214
280
  return;
215
281
  clearTimeout(timer);
216
282
  state.sessionRetryTimers.delete(sessionID);
217
- console.log(`[opencode-immune] Cancelled pending retry for session ${sessionID}: ${reason}`);
283
+ writePluginLog(state, "info", `[opencode-immune] Cancelled pending retry for session ${sessionID}: ${reason}`);
218
284
  }
219
285
  function cancelProviderRetryWatchdog(state, sessionID, reason) {
220
286
  const timer = state.providerRetryWatchdogs.get(sessionID);
@@ -222,7 +288,7 @@ function cancelProviderRetryWatchdog(state, sessionID, reason) {
222
288
  return;
223
289
  clearTimeout(timer);
224
290
  state.providerRetryWatchdogs.delete(sessionID);
225
- console.log(`[opencode-immune] Cancelled provider retry watchdog for session ${sessionID}: ${reason}`);
291
+ writePluginLog(state, "info", `[opencode-immune] Cancelled provider retry watchdog for session ${sessionID}: ${reason}`);
226
292
  }
227
293
  async function removeManagedUltraworkSession(state, sessionID, reason) {
228
294
  cancelPendingSessionRetry(state, sessionID, reason);
@@ -231,7 +297,7 @@ async function removeManagedUltraworkSession(state, sessionID, reason) {
231
297
  const existed = state.managedUltraworkSessions.delete(sessionID);
232
298
  if (!existed)
233
299
  return;
234
- console.log(`[opencode-immune] Removed managed ultrawork session ${sessionID}: ${reason}`);
300
+ writePluginLog(state, "info", `[opencode-immune] Removed managed ultrawork session ${sessionID}: ${reason}`);
235
301
  }
236
302
  async function updateManagedSessionAgent(state, sessionID, agent) {
237
303
  const existing = state.managedUltraworkSessions.get(sessionID);
@@ -493,7 +559,7 @@ async function sendManagedSessionRetryPrompt(state, sessionID, reason, options =
493
559
  },
494
560
  path: { id: sessionID },
495
561
  });
496
- console.log(`[opencode-immune] Retry prompt sent to session ${sessionID} (${reason})` +
562
+ writePluginLog(state, "info", `[opencode-immune] Retry prompt sent to session ${sessionID} (${reason})` +
497
563
  (fallbackModel
498
564
  ? ` using fallback model ${fallbackModel.providerID}/${fallbackModel.modelID}`
499
565
  : ""));
@@ -510,11 +576,11 @@ function scheduleManagedSessionRetry(state, sessionID, options) {
510
576
  return false;
511
577
  }
512
578
  if (state.sessionRetryTimers.has(sessionID)) {
513
- console.log(`[opencode-immune] Retry already pending for session ${sessionID}, skipping duplicate.`);
579
+ writePluginLog(state, "info", `[opencode-immune] Retry already pending for session ${sessionID}, skipping duplicate.`);
514
580
  return false;
515
581
  }
516
582
  const attemptInfo = options.attemptLabel ? ` (${options.attemptLabel})` : "";
517
- console.log(`[opencode-immune] Scheduling retry for session ${sessionID}${attemptInfo}. ` +
583
+ writePluginLog(state, "info", `[opencode-immune] Scheduling retry for session ${sessionID}${attemptInfo}. ` +
518
584
  `Waiting ${options.delayMs / 1000}s before retry...`);
519
585
  const timer = setTimeout(async () => {
520
586
  state.sessionRetryTimers.delete(sessionID);
@@ -530,7 +596,7 @@ function scheduleManagedSessionRetry(state, sessionID, options) {
530
596
  if (options.countAgainstBudget) {
531
597
  state.sessionErrorRetryCount.set(sessionID, Math.max((state.sessionErrorRetryCount.get(sessionID) ?? 1) - 1, 0));
532
598
  }
533
- console.log(`[opencode-immune] Retry prompt failed for session ${sessionID}. ` +
599
+ writePluginLog(state, "warn", `[opencode-immune] Retry prompt failed for session ${sessionID}. ` +
534
600
  `Will wait for the next retry signal.`);
535
601
  }
536
602
  }, options.delayMs);
@@ -544,13 +610,17 @@ function scheduleManagedSessionRetry(state, sessionID, options) {
544
610
  * Wraps a hook handler in a try/catch to prevent any single hook failure
545
611
  * from crashing the entire agent session.
546
612
  */
547
- function withErrorBoundary(hookName, handler) {
613
+ function withErrorBoundary(state, hookName, handler) {
548
614
  return (async (...args) => {
549
615
  try {
550
616
  return await handler(...args);
551
617
  }
552
618
  catch (err) {
553
- console.error(`[opencode-immune] Hook "${hookName}" error:`, err);
619
+ const hookInput = args.find((arg) => !!arg && typeof arg === "object" && "sessionID" in arg);
620
+ writePluginLog(state, "error", `[opencode-immune] Hook "${hookName}" error.`, {
621
+ error: normalizeLogValue(err),
622
+ sessionID: hookInput?.sessionID,
623
+ });
554
624
  // Error is swallowed — hook failure must not crash agent session
555
625
  }
556
626
  });
@@ -714,7 +784,7 @@ async function resolveEnvValue(directory, key) {
714
784
  * Fetch latest release info from the harness GitHub repo.
715
785
  * Returns null if token is missing, network fails, or no release found.
716
786
  */
717
- async function fetchLatestHarnessRelease(repo, token) {
787
+ async function fetchLatestHarnessRelease(directory, repo, token) {
718
788
  try {
719
789
  const url = `https://api.github.com/repos/${repo}/releases/latest`;
720
790
  const resp = await fetch(url, {
@@ -726,10 +796,10 @@ async function fetchLatestHarnessRelease(repo, token) {
726
796
  });
727
797
  if (!resp.ok) {
728
798
  if (resp.status === 404) {
729
- console.log(`[opencode-immune] Harness sync: no releases found in ${repo}`);
799
+ writePluginLogForDirectory(directory, "info", `[opencode-immune] Harness sync: no releases found in ${repo}`);
730
800
  }
731
801
  else {
732
- console.warn(`[opencode-immune] Harness sync: GitHub API returned ${resp.status} ${resp.statusText}`);
802
+ writePluginLogForDirectory(directory, "warn", `[opencode-immune] Harness sync: GitHub API returned ${resp.status} ${resp.statusText}`);
733
803
  }
734
804
  return null;
735
805
  }
@@ -742,7 +812,7 @@ async function fetchLatestHarnessRelease(repo, token) {
742
812
  const assetName = isWindows ? "harness-windows.tar.gz" : "harness.tar.gz";
743
813
  const asset = data.assets?.find((a) => a.name === assetName);
744
814
  if (!asset) {
745
- console.warn(`[opencode-immune] Harness sync: release ${tagName} has no ${assetName} asset`);
815
+ writePluginLogForDirectory(directory, "warn", `[opencode-immune] Harness sync: release ${tagName} has no ${assetName} asset`);
746
816
  return null;
747
817
  }
748
818
  return {
@@ -752,7 +822,7 @@ async function fetchLatestHarnessRelease(repo, token) {
752
822
  };
753
823
  }
754
824
  catch (err) {
755
- console.warn(`[opencode-immune] Harness sync: failed to fetch release info:`, err instanceof Error ? err.message : String(err));
825
+ writePluginLogForDirectory(directory, "warn", `[opencode-immune] Harness sync: failed to fetch release info.`, { error: normalizeLogValue(err) });
756
826
  return null;
757
827
  }
758
828
  }
@@ -817,7 +887,7 @@ async function copyDirRecursive(src, dest, skipRootFiles, rootDest) {
817
887
  for (const entry of entries) {
818
888
  // Skip files only at the root destination level
819
889
  if (skipRootFiles && dest === effectiveRoot && entry.name === ".gitignore") {
820
- console.log(`[opencode-immune] Harness sync: skipping root .gitignore`);
890
+ pluginLog.info(`[opencode-immune] Harness sync: skipping root .gitignore`);
821
891
  continue;
822
892
  }
823
893
  const srcPath = (0, path_1.join)(src, entry.name);
@@ -868,16 +938,16 @@ async function syncHarness(state) {
868
938
  || DEFAULT_HARNESS_REPO;
869
939
  try {
870
940
  // 1. Fetch latest release
871
- const release = await fetchLatestHarnessRelease(repo, token);
941
+ const release = await fetchLatestHarnessRelease(state.input.directory, repo, token);
872
942
  if (!release)
873
943
  return;
874
944
  // 2. Compare versions
875
945
  const localVersion = await readLocalHarnessVersion(state.input.directory);
876
946
  if (localVersion === release.tagName) {
877
- console.log(`[opencode-immune] Harness sync: already up to date (${release.tagName})`);
947
+ pluginLog.info(`[opencode-immune] Harness sync: already up to date (${release.tagName})`);
878
948
  return;
879
949
  }
880
- console.log(`[opencode-immune] Harness sync: updating ${localVersion ?? "(none)"} → ${release.tagName}`);
950
+ pluginLog.info(`[opencode-immune] Harness sync: updating ${localVersion ?? "(none)"} → ${release.tagName}`);
881
951
  // 3. Hash opencode.json before update
882
952
  const configPath = (0, path_1.join)(state.input.directory, "opencode.json");
883
953
  const hashBefore = await fileHash(configPath);
@@ -899,10 +969,10 @@ async function syncHarness(state) {
899
969
  // 8. Check if opencode.json changed
900
970
  const hashAfter = await fileHash(configPath);
901
971
  if (hashBefore && hashAfter && hashBefore !== hashAfter) {
902
- console.warn(`[opencode-immune] ⚠ Harness sync: opencode.json was updated. ` +
972
+ pluginLog.warn(`[opencode-immune] ⚠ Harness sync: opencode.json was updated. ` +
903
973
  `Please restart opencode for the new agent configuration to take effect.`);
904
974
  }
905
- console.log(`[opencode-immune] Harness sync: successfully updated to ${release.tagName}`);
975
+ pluginLog.info(`[opencode-immune] Harness sync: successfully updated to ${release.tagName}`);
906
976
  await writeDiagnosticLog(state, "harness-sync:success", {
907
977
  from: localVersion,
908
978
  to: release.tagName,
@@ -923,7 +993,7 @@ async function syncHarness(state) {
923
993
  }
924
994
  catch (err) {
925
995
  // Sync failure must never prevent plugin from working
926
- console.warn(`[opencode-immune] Harness sync failed:`, err instanceof Error ? err.message : String(err));
996
+ pluginLog.warn(`[opencode-immune] Harness sync failed:`, err instanceof Error ? err.message : String(err));
927
997
  await writeDiagnosticLog(state, "harness-sync:error", {
928
998
  error: err instanceof Error ? err.message : String(err),
929
999
  });
@@ -966,7 +1036,7 @@ function createTodoEnforcerChatMessage(state) {
966
1036
  // On user message, check previous assistant turn's counters
967
1037
  // then reset for next turn
968
1038
  if (state.toolCallCount > 3 && !state.todoWriteUsed) {
969
- console.warn(`[opencode-immune] Todo Enforcer: ${state.toolCallCount} tool calls without TodoWrite. ` +
1039
+ pluginLog.warn(`[opencode-immune] Todo Enforcer: ${state.toolCallCount} tool calls without TodoWrite. ` +
970
1040
  `Consider using todo list for multi-step tasks.`);
971
1041
  }
972
1042
  // Reset per-message counters for the next assistant turn
@@ -1008,20 +1078,20 @@ function createSessionRecoveryEvent(state) {
1008
1078
  }
1009
1079
  const markerActive = await isUltraworkMarkerActive(state);
1010
1080
  if (!markerActive) {
1011
- console.log(`[opencode-immune] Root session created (${sessionID}), no ultrawork marker — skipping auto-resume.`);
1081
+ pluginLog.info(`[opencode-immune] Root session created (${sessionID}), no ultrawork marker — skipping auto-resume.`);
1012
1082
  return;
1013
1083
  }
1014
- console.log(`[opencode-immune] Root session created (${sessionID}), ultrawork marker active — checking tasks.md...`);
1084
+ pluginLog.info(`[opencode-immune] Root session created (${sessionID}), ultrawork marker active — checking tasks.md...`);
1015
1085
  const recovery = await parseTasksFile(state.input.directory);
1016
1086
  if (recovery) {
1017
1087
  state.recoveryContext = recovery;
1018
- console.log(`[opencode-immune] Active task found: "${recovery.task}" (Level ${recovery.level}, Phase: ${recovery.phase})`);
1088
+ pluginLog.info(`[opencode-immune] Active task found: "${recovery.task}" (Level ${recovery.level}, Phase: ${recovery.phase})`);
1019
1089
  if (recovery.phase !== "ARCHIVE: DONE") {
1020
1090
  // Register this root session as managed so retry/recovery works
1021
1091
  await addManagedUltraworkSession(state, sessionID);
1022
1092
  // Skip sending AUTO-RESUME if already sent from plugin init
1023
1093
  if (state.autoResumeAttempted) {
1024
- console.log(`[opencode-immune] Auto-resume already sent from plugin init, skipping duplicate for session ${sessionID}.`);
1094
+ pluginLog.info(`[opencode-immune] Auto-resume already sent from plugin init, skipping duplicate for session ${sessionID}.`);
1025
1095
  return;
1026
1096
  }
1027
1097
  setTimeout(async () => {
@@ -1038,17 +1108,17 @@ function createSessionRecoveryEvent(state) {
1038
1108
  },
1039
1109
  path: { id: sessionID },
1040
1110
  });
1041
- console.log(`[opencode-immune] Auto-resume prompt sent to managed ultrawork session ${sessionID}`);
1111
+ pluginLog.info(`[opencode-immune] Auto-resume prompt sent to managed ultrawork session ${sessionID}`);
1042
1112
  }
1043
1113
  catch (err) {
1044
- console.log(`[opencode-immune] Auto-resume failed (session may have been taken over):`, err);
1114
+ pluginLog.info(`[opencode-immune] Auto-resume failed (session may have been taken over):`, err);
1045
1115
  }
1046
1116
  }, 3_000);
1047
1117
  }
1048
1118
  }
1049
1119
  else {
1050
1120
  state.recoveryContext = null;
1051
- console.log("[opencode-immune] No active task found.");
1121
+ pluginLog.info("[opencode-immune] No active task found.");
1052
1122
  }
1053
1123
  }
1054
1124
  };
@@ -1130,7 +1200,7 @@ function createRalphLoopToolAfter(state) {
1130
1200
  newString: input.args?.newString ?? "",
1131
1201
  timestamp: Date.now(),
1132
1202
  };
1133
- console.warn(`[opencode-immune] Ralph Loop: Edit failed for "${state.lastEditAttempt.filePath}". ` +
1203
+ pluginLog.warn(`[opencode-immune] Ralph Loop: Edit failed for "${state.lastEditAttempt.filePath}". ` +
1134
1204
  `Recovery hint will be injected in next system transform.`);
1135
1205
  }
1136
1206
  else {
@@ -1157,7 +1227,7 @@ function createContextMonitorChatMessage(state) {
1157
1227
  if (state.approximateTokens >
1158
1228
  ESTIMATED_CONTEXT_LIMIT * CONTEXT_WARNING_THRESHOLD) {
1159
1229
  const pct = Math.round((state.approximateTokens / ESTIMATED_CONTEXT_LIMIT) * 100);
1160
- console.warn(`[opencode-immune] Context Monitor: ~${state.approximateTokens} tokens estimated (${pct}% of ~${ESTIMATED_CONTEXT_LIMIT}). ` +
1230
+ pluginLog.warn(`[opencode-immune] Context Monitor: ~${state.approximateTokens} tokens estimated (${pct}% of ~${ESTIMATED_CONTEXT_LIMIT}). ` +
1161
1231
  `Consider compacting the session.`);
1162
1232
  }
1163
1233
  }
@@ -1180,7 +1250,7 @@ function createCompactionHandler(state) {
1180
1250
  "After compaction, the agent should still be able to resume the current phase without re-reading all Memory Bank files.");
1181
1251
  // Reset token counter after compaction
1182
1252
  state.approximateTokens = 0;
1183
- console.log("[opencode-immune] Context Monitor: Compaction triggered, token counter reset.");
1253
+ pluginLog.info("[opencode-immune] Context Monitor: Compaction triggered, token counter reset.");
1184
1254
  };
1185
1255
  }
1186
1256
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -1204,13 +1274,13 @@ function createCommentCheckerToolAfter(state) {
1204
1274
  return;
1205
1275
  // Check for emoji
1206
1276
  if (EMOJI_PATTERN.test(content)) {
1207
- console.warn(`[opencode-immune] Comment Checker: Emoji detected in ${input.tool} operation. ` +
1277
+ pluginLog.warn(`[opencode-immune] Comment Checker: Emoji detected in ${input.tool} operation. ` +
1208
1278
  `Avoid emojis in code unless the user explicitly requested them.`);
1209
1279
  }
1210
1280
  // Check for TODO/FIXME/HACK
1211
1281
  const todoMatch = content.match(TODO_PATTERN);
1212
1282
  if (todoMatch) {
1213
- console.warn(`[opencode-immune] Comment Checker: "${todoMatch[0]}" comment found in ${input.tool} operation. ` +
1283
+ pluginLog.warn(`[opencode-immune] Comment Checker: "${todoMatch[0]}" comment found in ${input.tool} operation. ` +
1214
1284
  `Consider resolving it or tracking it in the todo list.`);
1215
1285
  }
1216
1286
  };
@@ -1241,11 +1311,11 @@ function createKeywordDetectorChatMessage(_state) {
1241
1311
  if (!messageContent)
1242
1312
  return;
1243
1313
  if (ERROR_KEYWORDS.test(messageContent)) {
1244
- console.log(`[opencode-immune] Keyword Detector: Error-related keywords found. ` +
1314
+ pluginLog.info(`[opencode-immune] Keyword Detector: Error-related keywords found. ` +
1245
1315
  `Consider using 1-van to analyze the issue systematically.`);
1246
1316
  }
1247
1317
  if (DEPLOY_KEYWORDS.test(messageContent)) {
1248
- console.log(`[opencode-immune] Keyword Detector: Deploy/release keywords found. ` +
1318
+ pluginLog.info(`[opencode-immune] Keyword Detector: Deploy/release keywords found. ` +
1249
1319
  `Consider running 5-reflect first to verify implementation quality.`);
1250
1320
  }
1251
1321
  };
@@ -1274,7 +1344,7 @@ function createFallbackModels(state) {
1274
1344
  const recovery = await parseTasksFile(state.input.directory);
1275
1345
  if (recovery && recovery.phase !== "ARCHIVE: DONE") {
1276
1346
  state.recoveryContext = recovery;
1277
- console.log(`[opencode-immune] Auto-recovery on existing session: ` +
1347
+ pluginLog.info(`[opencode-immune] Auto-recovery on existing session: ` +
1278
1348
  `task="${recovery.task}", level=${recovery.level}, phase=${recovery.phase}. ` +
1279
1349
  `Sending AUTO-RESUME prompt in 3s...`);
1280
1350
  const sid = input.sessionID;
@@ -1292,10 +1362,10 @@ function createFallbackModels(state) {
1292
1362
  },
1293
1363
  path: { id: sid },
1294
1364
  });
1295
- console.log(`[opencode-immune] Auto-resume prompt sent to session ${sid}`);
1365
+ pluginLog.info(`[opencode-immune] Auto-resume prompt sent to session ${sid}`);
1296
1366
  }
1297
1367
  catch (err) {
1298
- console.log(`[opencode-immune] Auto-resume prompt failed:`, err);
1368
+ pluginLog.info(`[opencode-immune] Auto-resume prompt failed:`, err);
1299
1369
  }
1300
1370
  }, 3_000);
1301
1371
  }
@@ -1314,7 +1384,7 @@ function createFallbackModels(state) {
1314
1384
  const providerId = input.provider?.info && "id" in input.provider.info
1315
1385
  ? input.provider.info.id
1316
1386
  : "unknown";
1317
- console.log(`[opencode-immune] Model Observer: agent="${input.agent}", ` +
1387
+ pluginLog.info(`[opencode-immune] Model Observer: agent="${input.agent}", ` +
1318
1388
  `model="${modelId}", provider="${providerId}"`);
1319
1389
  };
1320
1390
  }
@@ -1352,7 +1422,7 @@ function createEventHandler(state) {
1352
1422
  if (count < MAX_RETRIES && !state.sessionRetryTimers.has(fallbackSessionID)) {
1353
1423
  const delay = Math.min(BASE_DELAY_MS * Math.pow(2, count), MAX_DELAY_MS);
1354
1424
  state.sessionErrorRetryCount.set(fallbackSessionID, count + 1);
1355
- console.warn(`[opencode-immune] session.error without sessionID matched retryable error. ` +
1425
+ pluginLog.warn(`[opencode-immune] session.error without sessionID matched retryable error. ` +
1356
1426
  `Retrying sole managed root session ${fallbackSessionID}.`);
1357
1427
  scheduleManagedSessionRetry(state, fallbackSessionID, {
1358
1428
  delayMs: delay,
@@ -1377,7 +1447,7 @@ function createEventHandler(state) {
1377
1447
  return;
1378
1448
  }
1379
1449
  if (state.sessionRetryTimers.has(sessionID)) {
1380
- console.log(`[opencode-immune] Retry already pending for ${isChild ? "child" : "root"} session ${sessionID}, skipping duplicate.`);
1450
+ pluginLog.info(`[opencode-immune] Retry already pending for ${isChild ? "child" : "root"} session ${sessionID}, skipping duplicate.`);
1381
1451
  return;
1382
1452
  }
1383
1453
  const count = state.sessionErrorRetryCount.get(sessionID) ?? 0;
@@ -1388,7 +1458,7 @@ function createEventHandler(state) {
1388
1458
  // child after the router advances can create two writers in one pipeline.
1389
1459
  if (isChild) {
1390
1460
  recordChildFallbackRequest(state, managedSession, sessionID, error);
1391
- console.log(`[opencode-immune] Child session ${sessionID}: retryable error detected. ` +
1461
+ pluginLog.info(`[opencode-immune] Child session ${sessionID}: retryable error detected. ` +
1392
1462
  `Recorded router-owned fallback request and skipped plugin auto-retry.`);
1393
1463
  state.sessionErrorRetryCount.set(sessionID, count);
1394
1464
  return;
@@ -1396,7 +1466,7 @@ function createEventHandler(state) {
1396
1466
  else if (isRoot && (isRateLimitApiError(error) || isCertificateApiError(error))) {
1397
1467
  await setSessionFallbackModel(state, sessionID, RATE_LIMIT_FALLBACK_MODEL);
1398
1468
  const errorType = isCertificateApiError(error) ? "certificate error" : "rate limit";
1399
- console.log(`[opencode-immune] ${errorType} detected for root session ${sessionID}. ` +
1469
+ pluginLog.info(`[opencode-immune] ${errorType} detected for root session ${sessionID}. ` +
1400
1470
  `Retry will use fallback model ${RATE_LIMIT_FALLBACK_MODEL.providerID}/${RATE_LIMIT_FALLBACK_MODEL.modelID}.`);
1401
1471
  }
1402
1472
  const scheduled = scheduleManagedSessionRetry(state, sessionID, {
@@ -1410,7 +1480,7 @@ function createEventHandler(state) {
1410
1480
  }
1411
1481
  }
1412
1482
  else {
1413
- console.log(`[opencode-immune] Max retries (${MAX_RETRIES}) reached for ${isChild ? "child" : "root"} session ${sessionID}. Not retrying.`);
1483
+ pluginLog.info(`[opencode-immune] Max retries (${MAX_RETRIES}) reached for ${isChild ? "child" : "root"} session ${sessionID}. Not retrying.`);
1414
1484
  }
1415
1485
  }
1416
1486
  // Reset retry counter on successful activity
@@ -1440,7 +1510,7 @@ function createEventHandler(state) {
1440
1510
  "file.edited",
1441
1511
  ];
1442
1512
  if (significantEvents.includes(eventType)) {
1443
- console.log(`[opencode-immune] Event: ${eventType}`);
1513
+ pluginLog.info(`[opencode-immune] Event: ${eventType}`);
1444
1514
  }
1445
1515
  };
1446
1516
  }
@@ -1466,7 +1536,7 @@ async function archiveProgress(directory) {
1466
1536
  const content = await (0, promises_1.readFile)(progressPath, "utf-8");
1467
1537
  // Skip if empty or trivially empty
1468
1538
  if (!content.trim() || content.trim() === "# Progress") {
1469
- console.log("[opencode-immune] Archive progress: nothing to archive (empty).");
1539
+ pluginLog.info("[opencode-immune] Archive progress: nothing to archive (empty).");
1470
1540
  return;
1471
1541
  }
1472
1542
  const archiveDir = (0, path_1.join)(directory, "memory-bank", "archive");
@@ -1477,13 +1547,13 @@ async function archiveProgress(directory) {
1477
1547
  const archiveName = `progress-${dateStr}-${ts}.md`;
1478
1548
  const archivePath = (0, path_1.join)(archiveDir, archiveName);
1479
1549
  await (0, promises_1.rename)(progressPath, archivePath);
1480
- console.log(`[opencode-immune] Archive progress: moved to ${archiveName}`);
1550
+ pluginLog.info(`[opencode-immune] Archive progress: moved to ${archiveName}`);
1481
1551
  }
1482
1552
  catch (err) {
1483
1553
  // File doesn't exist or move failed — not critical
1484
1554
  const msg = err instanceof Error ? err.message : String(err);
1485
1555
  if (!msg.includes("ENOENT")) {
1486
- console.warn("[opencode-immune] Archive progress failed:", msg);
1556
+ pluginLog.warn("[opencode-immune] Archive progress failed:", msg);
1487
1557
  }
1488
1558
  }
1489
1559
  }
@@ -1529,7 +1599,7 @@ function runGitCommit(directory) {
1529
1599
  // Stage all changes
1530
1600
  (0, child_process_1.execFile)("git", ["add", "-A"], { cwd: directory }, (addErr) => {
1531
1601
  if (addErr) {
1532
- console.error("[opencode-immune] git add failed:", addErr.message);
1602
+ pluginLog.error("[opencode-immune] git add failed:", addErr.message);
1533
1603
  resolve(false);
1534
1604
  return;
1535
1605
  }
@@ -1540,15 +1610,15 @@ function runGitCommit(directory) {
1540
1610
  (0, child_process_1.execFile)("git", ["commit", "-m", message], { cwd: directory }, (commitErr, stdout, stderr) => {
1541
1611
  if (commitErr) {
1542
1612
  if (stderr?.includes("nothing to commit") || stdout?.includes("nothing to commit")) {
1543
- console.log("[opencode-immune] git commit: nothing to commit (clean tree).");
1613
+ pluginLog.info("[opencode-immune] git commit: nothing to commit (clean tree).");
1544
1614
  resolve(true);
1545
1615
  return;
1546
1616
  }
1547
- console.error("[opencode-immune] git commit failed:", commitErr.message, stderr);
1617
+ pluginLog.error("[opencode-immune] git commit failed:", commitErr.message, stderr);
1548
1618
  resolve(false);
1549
1619
  return;
1550
1620
  }
1551
- console.log("[opencode-immune] git commit succeeded:", stdout?.trim());
1621
+ pluginLog.info("[opencode-immune] git commit succeeded:", stdout?.trim());
1552
1622
  resolve(true);
1553
1623
  });
1554
1624
  });
@@ -1583,25 +1653,25 @@ function createTextCompleteHandler(state) {
1583
1653
  countAgainstBudget: false,
1584
1654
  abortBeforePrompt: true,
1585
1655
  });
1586
- console.log(`[opencode-immune] Provider retry banner detected for session ${sessionID}. ` +
1656
+ pluginLog.info(`[opencode-immune] Provider retry banner detected for session ${sessionID}. ` +
1587
1657
  `Fallback model pinned to ${fallbackModel.providerID}/${fallbackModel.modelID}.`);
1588
1658
  }
1589
1659
  // ── ALL_CYCLES_COMPLETE: clear ultrawork marker ──
1590
1660
  if (text.includes(ALL_CYCLES_COMPLETE_MARKER)) {
1591
1661
  await clearUltraworkMarker(state);
1592
- console.log("[opencode-immune] Multi-Cycle: ALL_CYCLES_COMPLETE detected, marker cleared.");
1662
+ pluginLog.info("[opencode-immune] Multi-Cycle: ALL_CYCLES_COMPLETE detected, marker cleared.");
1593
1663
  return;
1594
1664
  }
1595
1665
  // ── PRE_COMMIT only (without CYCLE_COMPLETE in same part): run commit ──
1596
1666
  if (text.includes(PRE_COMMIT_MARKER) && !text.includes(CYCLE_COMPLETE_MARKER)) {
1597
1667
  if (!state.commitPending) {
1598
1668
  state.commitPending = true;
1599
- console.log("[opencode-immune] Multi-Cycle: PRE_COMMIT detected (standalone), running git commit...");
1669
+ pluginLog.info("[opencode-immune] Multi-Cycle: PRE_COMMIT detected (standalone), running git commit...");
1600
1670
  try {
1601
1671
  await runGitCommit(state.input.directory);
1602
1672
  }
1603
1673
  catch (err) {
1604
- console.error("[opencode-immune] Multi-Cycle: git commit failed (standalone):", err);
1674
+ pluginLog.error("[opencode-immune] Multi-Cycle: git commit failed (standalone):", err);
1605
1675
  }
1606
1676
  finally {
1607
1677
  state.commitPending = false;
@@ -1616,18 +1686,18 @@ function createTextCompleteHandler(state) {
1616
1686
  await archiveProgress(state.input.directory);
1617
1687
  }
1618
1688
  catch (err) {
1619
- console.warn("[opencode-immune] Multi-Cycle: archive progress failed:", err);
1689
+ pluginLog.warn("[opencode-immune] Multi-Cycle: archive progress failed:", err);
1620
1690
  }
1621
1691
  // Step 1: Always commit first
1622
1692
  if (!state.commitPending) {
1623
1693
  state.commitPending = true;
1624
- console.log("[opencode-immune] Multi-Cycle: CYCLE_COMPLETE detected, running git commit first...");
1694
+ pluginLog.info("[opencode-immune] Multi-Cycle: CYCLE_COMPLETE detected, running git commit first...");
1625
1695
  try {
1626
1696
  await runGitCommit(state.input.directory);
1627
- console.log("[opencode-immune] Multi-Cycle: git commit completed before new cycle.");
1697
+ pluginLog.info("[opencode-immune] Multi-Cycle: git commit completed before new cycle.");
1628
1698
  }
1629
1699
  catch (err) {
1630
- console.error("[opencode-immune] Multi-Cycle: git commit failed (continuing anyway):", err);
1700
+ pluginLog.error("[opencode-immune] Multi-Cycle: git commit failed (continuing anyway):", err);
1631
1701
  }
1632
1702
  finally {
1633
1703
  state.commitPending = false;
@@ -1636,13 +1706,13 @@ function createTextCompleteHandler(state) {
1636
1706
  // Step 2: Create new session
1637
1707
  state.cycleCount++;
1638
1708
  if (state.cycleCount >= MAX_CYCLES) {
1639
- console.log(`[opencode-immune] Multi-Cycle: MAX_CYCLES (${MAX_CYCLES}) reached. Not creating new session.`);
1709
+ pluginLog.info(`[opencode-immune] Multi-Cycle: MAX_CYCLES (${MAX_CYCLES}) reached. Not creating new session.`);
1640
1710
  await clearUltraworkMarker(state);
1641
1711
  return;
1642
1712
  }
1643
1713
  const taskMatch = text.match(NEXT_TASK_PATTERN);
1644
1714
  const nextTask = taskMatch?.[1]?.trim() ?? "Continue processing task backlog";
1645
- console.log(`[opencode-immune] Multi-Cycle: Creating new session (cycle ${state.cycleCount}/${MAX_CYCLES}) for: "${nextTask}"`);
1715
+ pluginLog.info(`[opencode-immune] Multi-Cycle: Creating new session (cycle ${state.cycleCount}/${MAX_CYCLES}) for: "${nextTask}"`);
1646
1716
  try {
1647
1717
  const createResult = await state.input.client.session.create({
1648
1718
  body: {
@@ -1652,10 +1722,10 @@ function createTextCompleteHandler(state) {
1652
1722
  const newSessionData = createResult?.data;
1653
1723
  const newSessionID = newSessionData?.id;
1654
1724
  if (!newSessionID) {
1655
- console.error("[opencode-immune] Multi-Cycle: Failed to create new session — no ID returned.");
1725
+ pluginLog.error("[opencode-immune] Multi-Cycle: Failed to create new session — no ID returned.");
1656
1726
  return;
1657
1727
  }
1658
- console.log(`[opencode-immune] Multi-Cycle: New session created: ${newSessionID}`);
1728
+ pluginLog.info(`[opencode-immune] Multi-Cycle: New session created: ${newSessionID}`);
1659
1729
  await addManagedUltraworkSession(state, newSessionID);
1660
1730
  await state.input.client.session.promptAsync({
1661
1731
  body: {
@@ -1669,10 +1739,10 @@ function createTextCompleteHandler(state) {
1669
1739
  },
1670
1740
  path: { id: newSessionID },
1671
1741
  });
1672
- console.log(`[opencode-immune] Multi-Cycle: Bootstrap prompt sent to ${newSessionID}`);
1742
+ pluginLog.info(`[opencode-immune] Multi-Cycle: Bootstrap prompt sent to ${newSessionID}`);
1673
1743
  }
1674
1744
  catch (err) {
1675
- console.error("[opencode-immune] Multi-Cycle: Failed to create session or send prompt:", err);
1745
+ pluginLog.error("[opencode-immune] Multi-Cycle: Failed to create session or send prompt:", err);
1676
1746
  }
1677
1747
  }
1678
1748
  };
@@ -1705,7 +1775,7 @@ function createMultiCycleHandler(state) {
1705
1775
  RATE_LIMIT_MESSAGE_PATTERN.test(messageContent)) {
1706
1776
  if (managedSession && !managedSession.fallbackModel) {
1707
1777
  await setSessionFallbackModel(state, sessionID, RATE_LIMIT_FALLBACK_MODEL);
1708
- console.log(`[opencode-immune] Rate limit message detected in chat output for session ${sessionID}. ` +
1778
+ pluginLog.info(`[opencode-immune] Rate limit message detected in chat output for session ${sessionID}. ` +
1709
1779
  `Fallback model pinned to ${RATE_LIMIT_FALLBACK_MODEL.providerID}/${RATE_LIMIT_FALLBACK_MODEL.modelID}.`);
1710
1780
  }
1711
1781
  if (managedSession) {
@@ -1730,7 +1800,7 @@ async function server(input) {
1730
1800
  // Runs in background so it doesn't delay plugin initialization.
1731
1801
  // If sync fails, plugin continues normally with existing config.
1732
1802
  syncHarness(state).catch((err) => {
1733
- console.warn(`[opencode-immune] Harness sync background error:`, err instanceof Error ? err.message : String(err));
1803
+ pluginLog.warn(`[opencode-immune] Harness sync background error:`, err instanceof Error ? err.message : String(err));
1734
1804
  });
1735
1805
  // Eagerly load recovery context at plugin init so it's available
1736
1806
  // for the very first system.transform call (before chat.params fires).
@@ -1741,7 +1811,7 @@ async function server(input) {
1741
1811
  // Active task exists with incomplete phases — resume it
1742
1812
  state.recoveryContext = recovery;
1743
1813
  state.autoResumeAttempted = true;
1744
- console.log(`[opencode-immune] Plugin init: ultrawork marker active, recovery context loaded: ` +
1814
+ pluginLog.info(`[opencode-immune] Plugin init: ultrawork marker active, recovery context loaded: ` +
1745
1815
  `task="${recovery.task}", level=${recovery.level}, phase=${recovery.phase}. ` +
1746
1816
  `Will create new session and send AUTO-RESUME.`);
1747
1817
  // Create a new session and send AUTO-RESUME prompt (same pattern as CYCLE_COMPLETE).
@@ -1756,10 +1826,10 @@ async function server(input) {
1756
1826
  const newSessionData = createResult?.data;
1757
1827
  const newSessionID = newSessionData?.id;
1758
1828
  if (!newSessionID) {
1759
- console.error("[opencode-immune] Auto-resume: Failed to create session — no session ID returned.");
1829
+ pluginLog.error("[opencode-immune] Auto-resume: Failed to create session — no session ID returned.");
1760
1830
  return;
1761
1831
  }
1762
- console.log(`[opencode-immune] Auto-resume: New session created: ${newSessionID}`);
1832
+ pluginLog.info(`[opencode-immune] Auto-resume: New session created: ${newSessionID}`);
1763
1833
  await addManagedUltraworkSession(state, newSessionID);
1764
1834
  await state.input.client.session.promptAsync({
1765
1835
  body: {
@@ -1773,10 +1843,10 @@ async function server(input) {
1773
1843
  },
1774
1844
  path: { id: newSessionID },
1775
1845
  });
1776
- console.log(`[opencode-immune] Auto-resume prompt sent to new session ${newSessionID}`);
1846
+ pluginLog.info(`[opencode-immune] Auto-resume prompt sent to new session ${newSessionID}`);
1777
1847
  }
1778
1848
  catch (err) {
1779
- console.error("[opencode-immune] Auto-resume: Failed to create session or send prompt:", err);
1849
+ pluginLog.error("[opencode-immune] Auto-resume: Failed to create session or send prompt:", err);
1780
1850
  }
1781
1851
  }, 5_000);
1782
1852
  }
@@ -1788,7 +1858,7 @@ async function server(input) {
1788
1858
  const hasPendingTasks = /- \[ \]/.test(backlogContent);
1789
1859
  if (hasPendingTasks) {
1790
1860
  state.autoResumeAttempted = true;
1791
- console.log(`[opencode-immune] Plugin init: no active task but backlog has pending items. ` +
1861
+ pluginLog.info(`[opencode-immune] Plugin init: no active task but backlog has pending items. ` +
1792
1862
  `Will create new session to start next cycle.`);
1793
1863
  setTimeout(async () => {
1794
1864
  try {
@@ -1800,10 +1870,10 @@ async function server(input) {
1800
1870
  const newSessionData = createResult?.data;
1801
1871
  const newSessionID = newSessionData?.id;
1802
1872
  if (!newSessionID) {
1803
- console.error("[opencode-immune] Auto-cycle: Failed to create session — no session ID returned.");
1873
+ pluginLog.error("[opencode-immune] Auto-cycle: Failed to create session — no session ID returned.");
1804
1874
  return;
1805
1875
  }
1806
- console.log(`[opencode-immune] Auto-cycle: New session created: ${newSessionID}`);
1876
+ pluginLog.info(`[opencode-immune] Auto-cycle: New session created: ${newSessionID}`);
1807
1877
  await addManagedUltraworkSession(state, newSessionID);
1808
1878
  await state.input.client.session.promptAsync({
1809
1879
  body: {
@@ -1817,27 +1887,27 @@ async function server(input) {
1817
1887
  },
1818
1888
  path: { id: newSessionID },
1819
1889
  });
1820
- console.log(`[opencode-immune] Auto-cycle prompt sent to new session ${newSessionID}`);
1890
+ pluginLog.info(`[opencode-immune] Auto-cycle prompt sent to new session ${newSessionID}`);
1821
1891
  }
1822
1892
  catch (err) {
1823
- console.error("[opencode-immune] Auto-cycle: Failed to create session or send prompt:", err);
1893
+ pluginLog.error("[opencode-immune] Auto-cycle: Failed to create session or send prompt:", err);
1824
1894
  }
1825
1895
  }, 5_000);
1826
1896
  }
1827
1897
  else {
1828
1898
  // No active task and no pending backlog — clear marker
1829
1899
  await clearUltraworkMarker(state);
1830
- console.log(`[opencode-immune] Plugin init: no active task and no pending backlog. Marker cleared.`);
1900
+ pluginLog.info(`[opencode-immune] Plugin init: no active task and no pending backlog. Marker cleared.`);
1831
1901
  }
1832
1902
  }
1833
1903
  catch {
1834
1904
  // backlog.md doesn't exist or can't be read — clear marker
1835
1905
  await clearUltraworkMarker(state);
1836
- console.log(`[opencode-immune] Plugin init: no active task, backlog unreadable. Marker cleared.`);
1906
+ pluginLog.info(`[opencode-immune] Plugin init: no active task, backlog unreadable. Marker cleared.`);
1837
1907
  }
1838
1908
  }
1839
1909
  }
1840
- console.log(`[opencode-immune] Plugin initialized. Directory: ${input.directory}`);
1910
+ pluginLog.info(`[opencode-immune] Plugin initialized. Directory: ${input.directory}`);
1841
1911
  // Compose tool.execute.after handlers:
1842
1912
  // Todo Enforcer (counter) + Ralph Loop (edit error) + Comment Checker
1843
1913
  const toolAfterHandlers = [
@@ -1854,13 +1924,13 @@ async function server(input) {
1854
1924
  createMultiCycleHandler(state),
1855
1925
  ];
1856
1926
  return {
1857
- event: withErrorBoundary("event", createEventHandler(state)),
1858
- "chat.message": withErrorBoundary("chat.message", compositeChatMessage(chatMessageHandlers)),
1859
- "chat.params": withErrorBoundary("chat.params", createFallbackModels(state)),
1860
- "tool.execute.after": withErrorBoundary("tool.execute.after", compositeToolAfter(toolAfterHandlers)),
1861
- "experimental.chat.system.transform": withErrorBoundary("experimental.chat.system.transform", createSystemTransform(state)),
1862
- "experimental.session.compacting": withErrorBoundary("experimental.session.compacting", createCompactionHandler(state)),
1863
- "experimental.text.complete": withErrorBoundary("experimental.text.complete", createTextCompleteHandler(state)),
1927
+ event: withErrorBoundary(state, "event", createEventHandler(state)),
1928
+ "chat.message": withErrorBoundary(state, "chat.message", compositeChatMessage(chatMessageHandlers)),
1929
+ "chat.params": withErrorBoundary(state, "chat.params", createFallbackModels(state)),
1930
+ "tool.execute.after": withErrorBoundary(state, "tool.execute.after", compositeToolAfter(toolAfterHandlers)),
1931
+ "experimental.chat.system.transform": withErrorBoundary(state, "experimental.chat.system.transform", createSystemTransform(state)),
1932
+ "experimental.session.compacting": withErrorBoundary(state, "experimental.session.compacting", createCompactionHandler(state)),
1933
+ "experimental.text.complete": withErrorBoundary(state, "experimental.text.complete", createTextCompleteHandler(state)),
1864
1934
  };
1865
1935
  }
1866
1936
  exports.default = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-immune",
3
- "version": "1.0.45",
3
+ "version": "1.0.46",
4
4
  "description": "OpenCode plugin: session recovery, auto-retry, multi-cycle automation, context monitoring",
5
5
  "exports": {
6
6
  "./server": "./dist/plugin.js"