opencode-immune 1.0.45 → 1.0.47

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 +243 -191
  2. package/package.json +3 -2
package/dist/plugin.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // Hybrid single-file architecture with factory functions, explicit state, error boundaries
4
4
  // See: memory-bank/creative/creative-plugin-architecture.md (Option C)
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ const client_1 = require("@opencode-ai/sdk/v2/client");
6
7
  const promises_1 = require("fs/promises");
7
8
  const path_1 = require("path");
8
9
  const crypto_1 = require("crypto");
@@ -59,10 +60,10 @@ async function checkPluginUpdate(state) {
59
60
  state.pluginUpdateMessage =
60
61
  `[PLUGIN UPDATE] opencode-immune ${currentVersion} → ${latest} is available. ` +
61
62
  `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}.`);
63
+ writePluginLog(state, "warn", `[opencode-immune] Plugin update available: ${currentVersion} → ${latest}.`);
63
64
  }
64
65
  else if (latest) {
65
- console.log(`[opencode-immune] Plugin version ${currentVersion} is up to date.`);
66
+ writePluginLog(state, "info", `[opencode-immune] Plugin version ${currentVersion} is up to date.`);
66
67
  }
67
68
  }
68
69
  catch {
@@ -70,8 +71,14 @@ async function checkPluginUpdate(state) {
70
71
  }
71
72
  }
72
73
  function createState(input) {
74
+ activeLogDirectory = input.directory;
75
+ const { client: _client, ...runtimeInput } = input;
73
76
  return {
74
- input,
77
+ input: runtimeInput,
78
+ client: (0, client_1.createOpencodeClient)({
79
+ baseUrl: input.serverUrl.toString(),
80
+ directory: input.directory,
81
+ }),
75
82
  recoveryContext: null,
76
83
  managedUltraworkSessions: new Map(),
77
84
  sessionRetryTimers: new Map(),
@@ -92,6 +99,25 @@ function createState(input) {
92
99
  };
93
100
  }
94
101
  const ULTRAWORK_AGENT = "0-ultrawork";
102
+ const ULTRAWORK_SESSION_PERMISSION = [
103
+ { permission: "read", pattern: "*", action: "allow" },
104
+ { permission: "edit", pattern: "*", action: "allow" },
105
+ { permission: "glob", pattern: "*", action: "allow" },
106
+ { permission: "grep", pattern: "*", action: "allow" },
107
+ { permission: "list", pattern: "*", action: "allow" },
108
+ { permission: "bash", pattern: "*", action: "allow" },
109
+ { permission: "task", pattern: "*", action: "allow" },
110
+ { permission: "external_directory", pattern: "*", action: "allow" },
111
+ { permission: "todowrite", pattern: "*", action: "allow" },
112
+ { permission: "question", pattern: "*", action: "allow" },
113
+ { permission: "webfetch", pattern: "*", action: "allow" },
114
+ { permission: "websearch", pattern: "*", action: "allow" },
115
+ { permission: "codesearch", pattern: "*", action: "allow" },
116
+ { permission: "lsp", pattern: "*", action: "allow" },
117
+ { permission: "skill", pattern: "*", action: "allow" },
118
+ ];
119
+ const DIAGNOSTIC_LOG_MAX_BYTES = 5 * 1024 * 1024;
120
+ let activeLogDirectory = null;
95
121
  const MANAGED_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
96
122
  const PROVIDER_RETRY_WATCHDOG_MS = 30_000;
97
123
  const CHILD_FALLBACK_REQUEST_TTL_MS = 10 * 60 * 1000;
@@ -113,6 +139,38 @@ function isManagedRootUltraworkSession(state, sessionID) {
113
139
  const record = getManagedSession(state, sessionID);
114
140
  return !!record && record.kind === "root";
115
141
  }
142
+ async function createManagedUltraworkSession(state, title) {
143
+ const result = await state.client.session.create({
144
+ directory: state.input.directory,
145
+ title,
146
+ permission: ULTRAWORK_SESSION_PERMISSION,
147
+ });
148
+ const sessionID = result?.data?.id;
149
+ if (!sessionID)
150
+ return null;
151
+ await addManagedUltraworkSession(state, sessionID);
152
+ return sessionID;
153
+ }
154
+ async function promptManagedSession(state, sessionID, text, options = {}) {
155
+ await state.client.session.promptAsync({
156
+ directory: state.input.directory,
157
+ sessionID,
158
+ ...(options.model ? { model: options.model } : {}),
159
+ agent: options.agent ?? ULTRAWORK_AGENT,
160
+ parts: [
161
+ {
162
+ type: "text",
163
+ text,
164
+ },
165
+ ],
166
+ });
167
+ }
168
+ async function abortManagedSession(state, sessionID) {
169
+ await state.client.session.abort({
170
+ directory: state.input.directory,
171
+ sessionID,
172
+ });
173
+ }
116
174
  function pruneExpiredManagedSessions(state, now = Date.now()) {
117
175
  let removed = 0;
118
176
  for (const [sessionID, record] of state.managedUltraworkSessions.entries()) {
@@ -130,6 +188,7 @@ async function writeDiagnosticLog(state, event, data = {}) {
130
188
  try {
131
189
  const cacheDir = (0, path_1.join)(state.input.directory, ".opencode", "state");
132
190
  await (0, promises_1.mkdir)(cacheDir, { recursive: true });
191
+ await rotateDiagnosticLogIfNeeded(state.diagnosticsLogPath);
133
192
  const line = JSON.stringify({ ts: new Date().toISOString(), event, ...data });
134
193
  await (0, promises_1.appendFile)(state.diagnosticsLogPath, `${line}\n`, "utf-8");
135
194
  }
@@ -137,6 +196,68 @@ async function writeDiagnosticLog(state, event, data = {}) {
137
196
  // diagnostics must never affect runtime behavior
138
197
  }
139
198
  }
199
+ async function rotateDiagnosticLogIfNeeded(logPath) {
200
+ try {
201
+ const current = await (0, promises_1.stat)(logPath);
202
+ if (current.size < DIAGNOSTIC_LOG_MAX_BYTES)
203
+ return;
204
+ const rotatedPath = `${logPath}.1`;
205
+ await (0, promises_1.rm)(rotatedPath, { force: true });
206
+ await (0, promises_1.rename)(logPath, rotatedPath);
207
+ }
208
+ catch {
209
+ // missing log or rotation failure must never affect runtime behavior
210
+ }
211
+ }
212
+ function normalizeLogValue(value) {
213
+ if (value instanceof Error) {
214
+ return {
215
+ name: value.name,
216
+ message: value.message,
217
+ stack: value.stack,
218
+ };
219
+ }
220
+ return value;
221
+ }
222
+ function writePluginLog(state, level, message, extra = {}) {
223
+ void writeDiagnosticLog(state, `log:${level}`, {
224
+ message,
225
+ ...extra,
226
+ });
227
+ }
228
+ function writePluginLogForDirectory(directory, level, message, extra = {}) {
229
+ const diagnosticsLogPath = (0, path_1.join)(directory, ".opencode", "state", "opencode-immune-debug.log");
230
+ void (async () => {
231
+ try {
232
+ await (0, promises_1.mkdir)((0, path_1.dirname)(diagnosticsLogPath), { recursive: true });
233
+ await rotateDiagnosticLogIfNeeded(diagnosticsLogPath);
234
+ const line = JSON.stringify({
235
+ ts: new Date().toISOString(),
236
+ event: `log:${level}`,
237
+ message,
238
+ ...extra,
239
+ });
240
+ await (0, promises_1.appendFile)(diagnosticsLogPath, `${line}\n`, "utf-8");
241
+ }
242
+ catch {
243
+ // file logging must never affect runtime behavior
244
+ }
245
+ })();
246
+ }
247
+ function writePluginLogFromArgs(level, values) {
248
+ if (!activeLogDirectory)
249
+ return;
250
+ const [first, ...rest] = values;
251
+ const message = typeof first === "string" ? first : JSON.stringify(normalizeLogValue(first));
252
+ writePluginLogForDirectory(activeLogDirectory, level, message, {
253
+ args: rest.map(normalizeLogValue),
254
+ });
255
+ }
256
+ const pluginLog = {
257
+ info: (...values) => writePluginLogFromArgs("info", values),
258
+ warn: (...values) => writePluginLogFromArgs("warn", values),
259
+ error: (...values) => writePluginLogFromArgs("error", values),
260
+ };
140
261
  // ── Ultrawork Marker File ──
141
262
  async function writeUltraworkMarker(state) {
142
263
  try {
@@ -214,7 +335,7 @@ function cancelPendingSessionRetry(state, sessionID, reason) {
214
335
  return;
215
336
  clearTimeout(timer);
216
337
  state.sessionRetryTimers.delete(sessionID);
217
- console.log(`[opencode-immune] Cancelled pending retry for session ${sessionID}: ${reason}`);
338
+ writePluginLog(state, "info", `[opencode-immune] Cancelled pending retry for session ${sessionID}: ${reason}`);
218
339
  }
219
340
  function cancelProviderRetryWatchdog(state, sessionID, reason) {
220
341
  const timer = state.providerRetryWatchdogs.get(sessionID);
@@ -222,7 +343,7 @@ function cancelProviderRetryWatchdog(state, sessionID, reason) {
222
343
  return;
223
344
  clearTimeout(timer);
224
345
  state.providerRetryWatchdogs.delete(sessionID);
225
- console.log(`[opencode-immune] Cancelled provider retry watchdog for session ${sessionID}: ${reason}`);
346
+ writePluginLog(state, "info", `[opencode-immune] Cancelled provider retry watchdog for session ${sessionID}: ${reason}`);
226
347
  }
227
348
  async function removeManagedUltraworkSession(state, sessionID, reason) {
228
349
  cancelPendingSessionRetry(state, sessionID, reason);
@@ -231,7 +352,7 @@ async function removeManagedUltraworkSession(state, sessionID, reason) {
231
352
  const existed = state.managedUltraworkSessions.delete(sessionID);
232
353
  if (!existed)
233
354
  return;
234
- console.log(`[opencode-immune] Removed managed ultrawork session ${sessionID}: ${reason}`);
355
+ writePluginLog(state, "info", `[opencode-immune] Removed managed ultrawork session ${sessionID}: ${reason}`);
235
356
  }
236
357
  async function updateManagedSessionAgent(state, sessionID, agent) {
237
358
  const existing = state.managedUltraworkSessions.get(sessionID);
@@ -468,9 +589,7 @@ async function sendManagedSessionRetryPrompt(state, sessionID, reason, options =
468
589
  });
469
590
  if (options.abortBeforePrompt) {
470
591
  try {
471
- await state.input.client.session.abort({
472
- path: { id: sessionID },
473
- });
592
+ await abortManagedSession(state, sessionID);
474
593
  await writeDiagnosticLog(state, "session-retry:abort-success", { sessionID });
475
594
  }
476
595
  catch (err) {
@@ -480,20 +599,11 @@ async function sendManagedSessionRetryPrompt(state, sessionID, reason, options =
480
599
  });
481
600
  }
482
601
  }
483
- await state.input.client.session.promptAsync({
484
- body: {
485
- ...(fallbackModel ? { model: fallbackModel } : {}),
486
- agent: retryAgent,
487
- parts: [
488
- {
489
- type: "text",
490
- text: retryText,
491
- },
492
- ],
493
- },
494
- path: { id: sessionID },
602
+ await promptManagedSession(state, sessionID, retryText, {
603
+ agent: retryAgent,
604
+ model: fallbackModel,
495
605
  });
496
- console.log(`[opencode-immune] Retry prompt sent to session ${sessionID} (${reason})` +
606
+ writePluginLog(state, "info", `[opencode-immune] Retry prompt sent to session ${sessionID} (${reason})` +
497
607
  (fallbackModel
498
608
  ? ` using fallback model ${fallbackModel.providerID}/${fallbackModel.modelID}`
499
609
  : ""));
@@ -510,11 +620,11 @@ function scheduleManagedSessionRetry(state, sessionID, options) {
510
620
  return false;
511
621
  }
512
622
  if (state.sessionRetryTimers.has(sessionID)) {
513
- console.log(`[opencode-immune] Retry already pending for session ${sessionID}, skipping duplicate.`);
623
+ writePluginLog(state, "info", `[opencode-immune] Retry already pending for session ${sessionID}, skipping duplicate.`);
514
624
  return false;
515
625
  }
516
626
  const attemptInfo = options.attemptLabel ? ` (${options.attemptLabel})` : "";
517
- console.log(`[opencode-immune] Scheduling retry for session ${sessionID}${attemptInfo}. ` +
627
+ writePluginLog(state, "info", `[opencode-immune] Scheduling retry for session ${sessionID}${attemptInfo}. ` +
518
628
  `Waiting ${options.delayMs / 1000}s before retry...`);
519
629
  const timer = setTimeout(async () => {
520
630
  state.sessionRetryTimers.delete(sessionID);
@@ -530,7 +640,7 @@ function scheduleManagedSessionRetry(state, sessionID, options) {
530
640
  if (options.countAgainstBudget) {
531
641
  state.sessionErrorRetryCount.set(sessionID, Math.max((state.sessionErrorRetryCount.get(sessionID) ?? 1) - 1, 0));
532
642
  }
533
- console.log(`[opencode-immune] Retry prompt failed for session ${sessionID}. ` +
643
+ writePluginLog(state, "warn", `[opencode-immune] Retry prompt failed for session ${sessionID}. ` +
534
644
  `Will wait for the next retry signal.`);
535
645
  }
536
646
  }, options.delayMs);
@@ -544,13 +654,17 @@ function scheduleManagedSessionRetry(state, sessionID, options) {
544
654
  * Wraps a hook handler in a try/catch to prevent any single hook failure
545
655
  * from crashing the entire agent session.
546
656
  */
547
- function withErrorBoundary(hookName, handler) {
657
+ function withErrorBoundary(state, hookName, handler) {
548
658
  return (async (...args) => {
549
659
  try {
550
660
  return await handler(...args);
551
661
  }
552
662
  catch (err) {
553
- console.error(`[opencode-immune] Hook "${hookName}" error:`, err);
663
+ const hookInput = args.find((arg) => !!arg && typeof arg === "object" && "sessionID" in arg);
664
+ writePluginLog(state, "error", `[opencode-immune] Hook "${hookName}" error.`, {
665
+ error: normalizeLogValue(err),
666
+ sessionID: hookInput?.sessionID,
667
+ });
554
668
  // Error is swallowed — hook failure must not crash agent session
555
669
  }
556
670
  });
@@ -714,7 +828,7 @@ async function resolveEnvValue(directory, key) {
714
828
  * Fetch latest release info from the harness GitHub repo.
715
829
  * Returns null if token is missing, network fails, or no release found.
716
830
  */
717
- async function fetchLatestHarnessRelease(repo, token) {
831
+ async function fetchLatestHarnessRelease(directory, repo, token) {
718
832
  try {
719
833
  const url = `https://api.github.com/repos/${repo}/releases/latest`;
720
834
  const resp = await fetch(url, {
@@ -726,10 +840,10 @@ async function fetchLatestHarnessRelease(repo, token) {
726
840
  });
727
841
  if (!resp.ok) {
728
842
  if (resp.status === 404) {
729
- console.log(`[opencode-immune] Harness sync: no releases found in ${repo}`);
843
+ writePluginLogForDirectory(directory, "info", `[opencode-immune] Harness sync: no releases found in ${repo}`);
730
844
  }
731
845
  else {
732
- console.warn(`[opencode-immune] Harness sync: GitHub API returned ${resp.status} ${resp.statusText}`);
846
+ writePluginLogForDirectory(directory, "warn", `[opencode-immune] Harness sync: GitHub API returned ${resp.status} ${resp.statusText}`);
733
847
  }
734
848
  return null;
735
849
  }
@@ -742,7 +856,7 @@ async function fetchLatestHarnessRelease(repo, token) {
742
856
  const assetName = isWindows ? "harness-windows.tar.gz" : "harness.tar.gz";
743
857
  const asset = data.assets?.find((a) => a.name === assetName);
744
858
  if (!asset) {
745
- console.warn(`[opencode-immune] Harness sync: release ${tagName} has no ${assetName} asset`);
859
+ writePluginLogForDirectory(directory, "warn", `[opencode-immune] Harness sync: release ${tagName} has no ${assetName} asset`);
746
860
  return null;
747
861
  }
748
862
  return {
@@ -752,7 +866,7 @@ async function fetchLatestHarnessRelease(repo, token) {
752
866
  };
753
867
  }
754
868
  catch (err) {
755
- console.warn(`[opencode-immune] Harness sync: failed to fetch release info:`, err instanceof Error ? err.message : String(err));
869
+ writePluginLogForDirectory(directory, "warn", `[opencode-immune] Harness sync: failed to fetch release info.`, { error: normalizeLogValue(err) });
756
870
  return null;
757
871
  }
758
872
  }
@@ -817,7 +931,7 @@ async function copyDirRecursive(src, dest, skipRootFiles, rootDest) {
817
931
  for (const entry of entries) {
818
932
  // Skip files only at the root destination level
819
933
  if (skipRootFiles && dest === effectiveRoot && entry.name === ".gitignore") {
820
- console.log(`[opencode-immune] Harness sync: skipping root .gitignore`);
934
+ pluginLog.info(`[opencode-immune] Harness sync: skipping root .gitignore`);
821
935
  continue;
822
936
  }
823
937
  const srcPath = (0, path_1.join)(src, entry.name);
@@ -868,16 +982,16 @@ async function syncHarness(state) {
868
982
  || DEFAULT_HARNESS_REPO;
869
983
  try {
870
984
  // 1. Fetch latest release
871
- const release = await fetchLatestHarnessRelease(repo, token);
985
+ const release = await fetchLatestHarnessRelease(state.input.directory, repo, token);
872
986
  if (!release)
873
987
  return;
874
988
  // 2. Compare versions
875
989
  const localVersion = await readLocalHarnessVersion(state.input.directory);
876
990
  if (localVersion === release.tagName) {
877
- console.log(`[opencode-immune] Harness sync: already up to date (${release.tagName})`);
991
+ pluginLog.info(`[opencode-immune] Harness sync: already up to date (${release.tagName})`);
878
992
  return;
879
993
  }
880
- console.log(`[opencode-immune] Harness sync: updating ${localVersion ?? "(none)"} → ${release.tagName}`);
994
+ pluginLog.info(`[opencode-immune] Harness sync: updating ${localVersion ?? "(none)"} → ${release.tagName}`);
881
995
  // 3. Hash opencode.json before update
882
996
  const configPath = (0, path_1.join)(state.input.directory, "opencode.json");
883
997
  const hashBefore = await fileHash(configPath);
@@ -899,10 +1013,10 @@ async function syncHarness(state) {
899
1013
  // 8. Check if opencode.json changed
900
1014
  const hashAfter = await fileHash(configPath);
901
1015
  if (hashBefore && hashAfter && hashBefore !== hashAfter) {
902
- console.warn(`[opencode-immune] ⚠ Harness sync: opencode.json was updated. ` +
1016
+ pluginLog.warn(`[opencode-immune] ⚠ Harness sync: opencode.json was updated. ` +
903
1017
  `Please restart opencode for the new agent configuration to take effect.`);
904
1018
  }
905
- console.log(`[opencode-immune] Harness sync: successfully updated to ${release.tagName}`);
1019
+ pluginLog.info(`[opencode-immune] Harness sync: successfully updated to ${release.tagName}`);
906
1020
  await writeDiagnosticLog(state, "harness-sync:success", {
907
1021
  from: localVersion,
908
1022
  to: release.tagName,
@@ -923,7 +1037,7 @@ async function syncHarness(state) {
923
1037
  }
924
1038
  catch (err) {
925
1039
  // Sync failure must never prevent plugin from working
926
- console.warn(`[opencode-immune] Harness sync failed:`, err instanceof Error ? err.message : String(err));
1040
+ pluginLog.warn(`[opencode-immune] Harness sync failed:`, err instanceof Error ? err.message : String(err));
927
1041
  await writeDiagnosticLog(state, "harness-sync:error", {
928
1042
  error: err instanceof Error ? err.message : String(err),
929
1043
  });
@@ -966,7 +1080,7 @@ function createTodoEnforcerChatMessage(state) {
966
1080
  // On user message, check previous assistant turn's counters
967
1081
  // then reset for next turn
968
1082
  if (state.toolCallCount > 3 && !state.todoWriteUsed) {
969
- console.warn(`[opencode-immune] Todo Enforcer: ${state.toolCallCount} tool calls without TodoWrite. ` +
1083
+ pluginLog.warn(`[opencode-immune] Todo Enforcer: ${state.toolCallCount} tool calls without TodoWrite. ` +
970
1084
  `Consider using todo list for multi-step tasks.`);
971
1085
  }
972
1086
  // Reset per-message counters for the next assistant turn
@@ -1008,47 +1122,36 @@ function createSessionRecoveryEvent(state) {
1008
1122
  }
1009
1123
  const markerActive = await isUltraworkMarkerActive(state);
1010
1124
  if (!markerActive) {
1011
- console.log(`[opencode-immune] Root session created (${sessionID}), no ultrawork marker — skipping auto-resume.`);
1125
+ pluginLog.info(`[opencode-immune] Root session created (${sessionID}), no ultrawork marker — skipping auto-resume.`);
1012
1126
  return;
1013
1127
  }
1014
- console.log(`[opencode-immune] Root session created (${sessionID}), ultrawork marker active — checking tasks.md...`);
1128
+ pluginLog.info(`[opencode-immune] Root session created (${sessionID}), ultrawork marker active — checking tasks.md...`);
1015
1129
  const recovery = await parseTasksFile(state.input.directory);
1016
1130
  if (recovery) {
1017
1131
  state.recoveryContext = recovery;
1018
- console.log(`[opencode-immune] Active task found: "${recovery.task}" (Level ${recovery.level}, Phase: ${recovery.phase})`);
1132
+ pluginLog.info(`[opencode-immune] Active task found: "${recovery.task}" (Level ${recovery.level}, Phase: ${recovery.phase})`);
1019
1133
  if (recovery.phase !== "ARCHIVE: DONE") {
1020
1134
  // Register this root session as managed so retry/recovery works
1021
1135
  await addManagedUltraworkSession(state, sessionID);
1022
1136
  // Skip sending AUTO-RESUME if already sent from plugin init
1023
1137
  if (state.autoResumeAttempted) {
1024
- console.log(`[opencode-immune] Auto-resume already sent from plugin init, skipping duplicate for session ${sessionID}.`);
1138
+ pluginLog.info(`[opencode-immune] Auto-resume already sent from plugin init, skipping duplicate for session ${sessionID}.`);
1025
1139
  return;
1026
1140
  }
1027
1141
  setTimeout(async () => {
1028
1142
  try {
1029
- await state.input.client.session.promptAsync({
1030
- body: {
1031
- agent: ULTRAWORK_AGENT,
1032
- parts: [
1033
- {
1034
- type: "text",
1035
- text: `[AUTO-RESUME] Previous session was interrupted. Read memory-bank/tasks.md, check the Phase Status block, and continue the pipeline. Use ONLY the Phase Status block to determine the next phase. Do NOT analyze or evaluate the content of tasks.md. Call the appropriate router with the exact neutral prompt from your Step 5 table.`,
1036
- },
1037
- ],
1038
- },
1039
- path: { id: sessionID },
1040
- });
1041
- console.log(`[opencode-immune] Auto-resume prompt sent to managed ultrawork session ${sessionID}`);
1143
+ await promptManagedSession(state, sessionID, `[AUTO-RESUME] Previous session was interrupted. Read memory-bank/tasks.md, check the Phase Status block, and continue the pipeline. Use ONLY the Phase Status block to determine the next phase. Do NOT analyze or evaluate the content of tasks.md. Call the appropriate router with the exact neutral prompt from your Step 5 table.`);
1144
+ pluginLog.info(`[opencode-immune] Auto-resume prompt sent to managed ultrawork session ${sessionID}`);
1042
1145
  }
1043
1146
  catch (err) {
1044
- console.log(`[opencode-immune] Auto-resume failed (session may have been taken over):`, err);
1147
+ pluginLog.info(`[opencode-immune] Auto-resume failed (session may have been taken over):`, err);
1045
1148
  }
1046
1149
  }, 3_000);
1047
1150
  }
1048
1151
  }
1049
1152
  else {
1050
1153
  state.recoveryContext = null;
1051
- console.log("[opencode-immune] No active task found.");
1154
+ pluginLog.info("[opencode-immune] No active task found.");
1052
1155
  }
1053
1156
  }
1054
1157
  };
@@ -1130,7 +1233,7 @@ function createRalphLoopToolAfter(state) {
1130
1233
  newString: input.args?.newString ?? "",
1131
1234
  timestamp: Date.now(),
1132
1235
  };
1133
- console.warn(`[opencode-immune] Ralph Loop: Edit failed for "${state.lastEditAttempt.filePath}". ` +
1236
+ pluginLog.warn(`[opencode-immune] Ralph Loop: Edit failed for "${state.lastEditAttempt.filePath}". ` +
1134
1237
  `Recovery hint will be injected in next system transform.`);
1135
1238
  }
1136
1239
  else {
@@ -1157,7 +1260,7 @@ function createContextMonitorChatMessage(state) {
1157
1260
  if (state.approximateTokens >
1158
1261
  ESTIMATED_CONTEXT_LIMIT * CONTEXT_WARNING_THRESHOLD) {
1159
1262
  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}). ` +
1263
+ pluginLog.warn(`[opencode-immune] Context Monitor: ~${state.approximateTokens} tokens estimated (${pct}% of ~${ESTIMATED_CONTEXT_LIMIT}). ` +
1161
1264
  `Consider compacting the session.`);
1162
1265
  }
1163
1266
  }
@@ -1180,7 +1283,7 @@ function createCompactionHandler(state) {
1180
1283
  "After compaction, the agent should still be able to resume the current phase without re-reading all Memory Bank files.");
1181
1284
  // Reset token counter after compaction
1182
1285
  state.approximateTokens = 0;
1183
- console.log("[opencode-immune] Context Monitor: Compaction triggered, token counter reset.");
1286
+ pluginLog.info("[opencode-immune] Context Monitor: Compaction triggered, token counter reset.");
1184
1287
  };
1185
1288
  }
1186
1289
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -1204,13 +1307,13 @@ function createCommentCheckerToolAfter(state) {
1204
1307
  return;
1205
1308
  // Check for emoji
1206
1309
  if (EMOJI_PATTERN.test(content)) {
1207
- console.warn(`[opencode-immune] Comment Checker: Emoji detected in ${input.tool} operation. ` +
1310
+ pluginLog.warn(`[opencode-immune] Comment Checker: Emoji detected in ${input.tool} operation. ` +
1208
1311
  `Avoid emojis in code unless the user explicitly requested them.`);
1209
1312
  }
1210
1313
  // Check for TODO/FIXME/HACK
1211
1314
  const todoMatch = content.match(TODO_PATTERN);
1212
1315
  if (todoMatch) {
1213
- console.warn(`[opencode-immune] Comment Checker: "${todoMatch[0]}" comment found in ${input.tool} operation. ` +
1316
+ pluginLog.warn(`[opencode-immune] Comment Checker: "${todoMatch[0]}" comment found in ${input.tool} operation. ` +
1214
1317
  `Consider resolving it or tracking it in the todo list.`);
1215
1318
  }
1216
1319
  };
@@ -1241,11 +1344,11 @@ function createKeywordDetectorChatMessage(_state) {
1241
1344
  if (!messageContent)
1242
1345
  return;
1243
1346
  if (ERROR_KEYWORDS.test(messageContent)) {
1244
- console.log(`[opencode-immune] Keyword Detector: Error-related keywords found. ` +
1347
+ pluginLog.info(`[opencode-immune] Keyword Detector: Error-related keywords found. ` +
1245
1348
  `Consider using 1-van to analyze the issue systematically.`);
1246
1349
  }
1247
1350
  if (DEPLOY_KEYWORDS.test(messageContent)) {
1248
- console.log(`[opencode-immune] Keyword Detector: Deploy/release keywords found. ` +
1351
+ pluginLog.info(`[opencode-immune] Keyword Detector: Deploy/release keywords found. ` +
1249
1352
  `Consider running 5-reflect first to verify implementation quality.`);
1250
1353
  }
1251
1354
  };
@@ -1274,28 +1377,17 @@ function createFallbackModels(state) {
1274
1377
  const recovery = await parseTasksFile(state.input.directory);
1275
1378
  if (recovery && recovery.phase !== "ARCHIVE: DONE") {
1276
1379
  state.recoveryContext = recovery;
1277
- console.log(`[opencode-immune] Auto-recovery on existing session: ` +
1380
+ pluginLog.info(`[opencode-immune] Auto-recovery on existing session: ` +
1278
1381
  `task="${recovery.task}", level=${recovery.level}, phase=${recovery.phase}. ` +
1279
1382
  `Sending AUTO-RESUME prompt in 3s...`);
1280
1383
  const sid = input.sessionID;
1281
1384
  setTimeout(async () => {
1282
1385
  try {
1283
- await state.input.client.session.promptAsync({
1284
- body: {
1285
- agent: ULTRAWORK_AGENT,
1286
- parts: [
1287
- {
1288
- type: "text",
1289
- text: `[AUTO-RESUME] Previous session was interrupted. Read memory-bank/tasks.md, check the Phase Status block, and continue the pipeline. Use ONLY the Phase Status block to determine the next phase. Do NOT analyze or evaluate the content of tasks.md. Call the appropriate router with the exact neutral prompt from your Step 5 table.`,
1290
- },
1291
- ],
1292
- },
1293
- path: { id: sid },
1294
- });
1295
- console.log(`[opencode-immune] Auto-resume prompt sent to session ${sid}`);
1386
+ await promptManagedSession(state, sid, `[AUTO-RESUME] Previous session was interrupted. Read memory-bank/tasks.md, check the Phase Status block, and continue the pipeline. Use ONLY the Phase Status block to determine the next phase. Do NOT analyze or evaluate the content of tasks.md. Call the appropriate router with the exact neutral prompt from your Step 5 table.`);
1387
+ pluginLog.info(`[opencode-immune] Auto-resume prompt sent to session ${sid}`);
1296
1388
  }
1297
1389
  catch (err) {
1298
- console.log(`[opencode-immune] Auto-resume prompt failed:`, err);
1390
+ pluginLog.info(`[opencode-immune] Auto-resume prompt failed:`, err);
1299
1391
  }
1300
1392
  }, 3_000);
1301
1393
  }
@@ -1314,7 +1406,7 @@ function createFallbackModels(state) {
1314
1406
  const providerId = input.provider?.info && "id" in input.provider.info
1315
1407
  ? input.provider.info.id
1316
1408
  : "unknown";
1317
- console.log(`[opencode-immune] Model Observer: agent="${input.agent}", ` +
1409
+ pluginLog.info(`[opencode-immune] Model Observer: agent="${input.agent}", ` +
1318
1410
  `model="${modelId}", provider="${providerId}"`);
1319
1411
  };
1320
1412
  }
@@ -1352,7 +1444,7 @@ function createEventHandler(state) {
1352
1444
  if (count < MAX_RETRIES && !state.sessionRetryTimers.has(fallbackSessionID)) {
1353
1445
  const delay = Math.min(BASE_DELAY_MS * Math.pow(2, count), MAX_DELAY_MS);
1354
1446
  state.sessionErrorRetryCount.set(fallbackSessionID, count + 1);
1355
- console.warn(`[opencode-immune] session.error without sessionID matched retryable error. ` +
1447
+ pluginLog.warn(`[opencode-immune] session.error without sessionID matched retryable error. ` +
1356
1448
  `Retrying sole managed root session ${fallbackSessionID}.`);
1357
1449
  scheduleManagedSessionRetry(state, fallbackSessionID, {
1358
1450
  delayMs: delay,
@@ -1377,7 +1469,7 @@ function createEventHandler(state) {
1377
1469
  return;
1378
1470
  }
1379
1471
  if (state.sessionRetryTimers.has(sessionID)) {
1380
- console.log(`[opencode-immune] Retry already pending for ${isChild ? "child" : "root"} session ${sessionID}, skipping duplicate.`);
1472
+ pluginLog.info(`[opencode-immune] Retry already pending for ${isChild ? "child" : "root"} session ${sessionID}, skipping duplicate.`);
1381
1473
  return;
1382
1474
  }
1383
1475
  const count = state.sessionErrorRetryCount.get(sessionID) ?? 0;
@@ -1388,7 +1480,7 @@ function createEventHandler(state) {
1388
1480
  // child after the router advances can create two writers in one pipeline.
1389
1481
  if (isChild) {
1390
1482
  recordChildFallbackRequest(state, managedSession, sessionID, error);
1391
- console.log(`[opencode-immune] Child session ${sessionID}: retryable error detected. ` +
1483
+ pluginLog.info(`[opencode-immune] Child session ${sessionID}: retryable error detected. ` +
1392
1484
  `Recorded router-owned fallback request and skipped plugin auto-retry.`);
1393
1485
  state.sessionErrorRetryCount.set(sessionID, count);
1394
1486
  return;
@@ -1396,7 +1488,7 @@ function createEventHandler(state) {
1396
1488
  else if (isRoot && (isRateLimitApiError(error) || isCertificateApiError(error))) {
1397
1489
  await setSessionFallbackModel(state, sessionID, RATE_LIMIT_FALLBACK_MODEL);
1398
1490
  const errorType = isCertificateApiError(error) ? "certificate error" : "rate limit";
1399
- console.log(`[opencode-immune] ${errorType} detected for root session ${sessionID}. ` +
1491
+ pluginLog.info(`[opencode-immune] ${errorType} detected for root session ${sessionID}. ` +
1400
1492
  `Retry will use fallback model ${RATE_LIMIT_FALLBACK_MODEL.providerID}/${RATE_LIMIT_FALLBACK_MODEL.modelID}.`);
1401
1493
  }
1402
1494
  const scheduled = scheduleManagedSessionRetry(state, sessionID, {
@@ -1410,7 +1502,7 @@ function createEventHandler(state) {
1410
1502
  }
1411
1503
  }
1412
1504
  else {
1413
- console.log(`[opencode-immune] Max retries (${MAX_RETRIES}) reached for ${isChild ? "child" : "root"} session ${sessionID}. Not retrying.`);
1505
+ pluginLog.info(`[opencode-immune] Max retries (${MAX_RETRIES}) reached for ${isChild ? "child" : "root"} session ${sessionID}. Not retrying.`);
1414
1506
  }
1415
1507
  }
1416
1508
  // Reset retry counter on successful activity
@@ -1440,7 +1532,7 @@ function createEventHandler(state) {
1440
1532
  "file.edited",
1441
1533
  ];
1442
1534
  if (significantEvents.includes(eventType)) {
1443
- console.log(`[opencode-immune] Event: ${eventType}`);
1535
+ pluginLog.info(`[opencode-immune] Event: ${eventType}`);
1444
1536
  }
1445
1537
  };
1446
1538
  }
@@ -1466,7 +1558,7 @@ async function archiveProgress(directory) {
1466
1558
  const content = await (0, promises_1.readFile)(progressPath, "utf-8");
1467
1559
  // Skip if empty or trivially empty
1468
1560
  if (!content.trim() || content.trim() === "# Progress") {
1469
- console.log("[opencode-immune] Archive progress: nothing to archive (empty).");
1561
+ pluginLog.info("[opencode-immune] Archive progress: nothing to archive (empty).");
1470
1562
  return;
1471
1563
  }
1472
1564
  const archiveDir = (0, path_1.join)(directory, "memory-bank", "archive");
@@ -1477,13 +1569,13 @@ async function archiveProgress(directory) {
1477
1569
  const archiveName = `progress-${dateStr}-${ts}.md`;
1478
1570
  const archivePath = (0, path_1.join)(archiveDir, archiveName);
1479
1571
  await (0, promises_1.rename)(progressPath, archivePath);
1480
- console.log(`[opencode-immune] Archive progress: moved to ${archiveName}`);
1572
+ pluginLog.info(`[opencode-immune] Archive progress: moved to ${archiveName}`);
1481
1573
  }
1482
1574
  catch (err) {
1483
1575
  // File doesn't exist or move failed — not critical
1484
1576
  const msg = err instanceof Error ? err.message : String(err);
1485
1577
  if (!msg.includes("ENOENT")) {
1486
- console.warn("[opencode-immune] Archive progress failed:", msg);
1578
+ pluginLog.warn("[opencode-immune] Archive progress failed:", msg);
1487
1579
  }
1488
1580
  }
1489
1581
  }
@@ -1529,7 +1621,7 @@ function runGitCommit(directory) {
1529
1621
  // Stage all changes
1530
1622
  (0, child_process_1.execFile)("git", ["add", "-A"], { cwd: directory }, (addErr) => {
1531
1623
  if (addErr) {
1532
- console.error("[opencode-immune] git add failed:", addErr.message);
1624
+ pluginLog.error("[opencode-immune] git add failed:", addErr.message);
1533
1625
  resolve(false);
1534
1626
  return;
1535
1627
  }
@@ -1540,15 +1632,15 @@ function runGitCommit(directory) {
1540
1632
  (0, child_process_1.execFile)("git", ["commit", "-m", message], { cwd: directory }, (commitErr, stdout, stderr) => {
1541
1633
  if (commitErr) {
1542
1634
  if (stderr?.includes("nothing to commit") || stdout?.includes("nothing to commit")) {
1543
- console.log("[opencode-immune] git commit: nothing to commit (clean tree).");
1635
+ pluginLog.info("[opencode-immune] git commit: nothing to commit (clean tree).");
1544
1636
  resolve(true);
1545
1637
  return;
1546
1638
  }
1547
- console.error("[opencode-immune] git commit failed:", commitErr.message, stderr);
1639
+ pluginLog.error("[opencode-immune] git commit failed:", commitErr.message, stderr);
1548
1640
  resolve(false);
1549
1641
  return;
1550
1642
  }
1551
- console.log("[opencode-immune] git commit succeeded:", stdout?.trim());
1643
+ pluginLog.info("[opencode-immune] git commit succeeded:", stdout?.trim());
1552
1644
  resolve(true);
1553
1645
  });
1554
1646
  });
@@ -1583,25 +1675,25 @@ function createTextCompleteHandler(state) {
1583
1675
  countAgainstBudget: false,
1584
1676
  abortBeforePrompt: true,
1585
1677
  });
1586
- console.log(`[opencode-immune] Provider retry banner detected for session ${sessionID}. ` +
1678
+ pluginLog.info(`[opencode-immune] Provider retry banner detected for session ${sessionID}. ` +
1587
1679
  `Fallback model pinned to ${fallbackModel.providerID}/${fallbackModel.modelID}.`);
1588
1680
  }
1589
1681
  // ── ALL_CYCLES_COMPLETE: clear ultrawork marker ──
1590
1682
  if (text.includes(ALL_CYCLES_COMPLETE_MARKER)) {
1591
1683
  await clearUltraworkMarker(state);
1592
- console.log("[opencode-immune] Multi-Cycle: ALL_CYCLES_COMPLETE detected, marker cleared.");
1684
+ pluginLog.info("[opencode-immune] Multi-Cycle: ALL_CYCLES_COMPLETE detected, marker cleared.");
1593
1685
  return;
1594
1686
  }
1595
1687
  // ── PRE_COMMIT only (without CYCLE_COMPLETE in same part): run commit ──
1596
1688
  if (text.includes(PRE_COMMIT_MARKER) && !text.includes(CYCLE_COMPLETE_MARKER)) {
1597
1689
  if (!state.commitPending) {
1598
1690
  state.commitPending = true;
1599
- console.log("[opencode-immune] Multi-Cycle: PRE_COMMIT detected (standalone), running git commit...");
1691
+ pluginLog.info("[opencode-immune] Multi-Cycle: PRE_COMMIT detected (standalone), running git commit...");
1600
1692
  try {
1601
1693
  await runGitCommit(state.input.directory);
1602
1694
  }
1603
1695
  catch (err) {
1604
- console.error("[opencode-immune] Multi-Cycle: git commit failed (standalone):", err);
1696
+ pluginLog.error("[opencode-immune] Multi-Cycle: git commit failed (standalone):", err);
1605
1697
  }
1606
1698
  finally {
1607
1699
  state.commitPending = false;
@@ -1616,18 +1708,18 @@ function createTextCompleteHandler(state) {
1616
1708
  await archiveProgress(state.input.directory);
1617
1709
  }
1618
1710
  catch (err) {
1619
- console.warn("[opencode-immune] Multi-Cycle: archive progress failed:", err);
1711
+ pluginLog.warn("[opencode-immune] Multi-Cycle: archive progress failed:", err);
1620
1712
  }
1621
1713
  // Step 1: Always commit first
1622
1714
  if (!state.commitPending) {
1623
1715
  state.commitPending = true;
1624
- console.log("[opencode-immune] Multi-Cycle: CYCLE_COMPLETE detected, running git commit first...");
1716
+ pluginLog.info("[opencode-immune] Multi-Cycle: CYCLE_COMPLETE detected, running git commit first...");
1625
1717
  try {
1626
1718
  await runGitCommit(state.input.directory);
1627
- console.log("[opencode-immune] Multi-Cycle: git commit completed before new cycle.");
1719
+ pluginLog.info("[opencode-immune] Multi-Cycle: git commit completed before new cycle.");
1628
1720
  }
1629
1721
  catch (err) {
1630
- console.error("[opencode-immune] Multi-Cycle: git commit failed (continuing anyway):", err);
1722
+ pluginLog.error("[opencode-immune] Multi-Cycle: git commit failed (continuing anyway):", err);
1631
1723
  }
1632
1724
  finally {
1633
1725
  state.commitPending = false;
@@ -1636,43 +1728,25 @@ function createTextCompleteHandler(state) {
1636
1728
  // Step 2: Create new session
1637
1729
  state.cycleCount++;
1638
1730
  if (state.cycleCount >= MAX_CYCLES) {
1639
- console.log(`[opencode-immune] Multi-Cycle: MAX_CYCLES (${MAX_CYCLES}) reached. Not creating new session.`);
1731
+ pluginLog.info(`[opencode-immune] Multi-Cycle: MAX_CYCLES (${MAX_CYCLES}) reached. Not creating new session.`);
1640
1732
  await clearUltraworkMarker(state);
1641
1733
  return;
1642
1734
  }
1643
1735
  const taskMatch = text.match(NEXT_TASK_PATTERN);
1644
1736
  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}"`);
1737
+ pluginLog.info(`[opencode-immune] Multi-Cycle: Creating new session (cycle ${state.cycleCount}/${MAX_CYCLES}) for: "${nextTask}"`);
1646
1738
  try {
1647
- const createResult = await state.input.client.session.create({
1648
- body: {
1649
- title: `Ultrawork Cycle ${state.cycleCount + 1}`,
1650
- },
1651
- });
1652
- const newSessionData = createResult?.data;
1653
- const newSessionID = newSessionData?.id;
1739
+ const newSessionID = await createManagedUltraworkSession(state, `Ultrawork Cycle ${state.cycleCount + 1}`);
1654
1740
  if (!newSessionID) {
1655
- console.error("[opencode-immune] Multi-Cycle: Failed to create new session — no ID returned.");
1741
+ pluginLog.error("[opencode-immune] Multi-Cycle: Failed to create new session — no ID returned.");
1656
1742
  return;
1657
1743
  }
1658
- console.log(`[opencode-immune] Multi-Cycle: New session created: ${newSessionID}`);
1659
- await addManagedUltraworkSession(state, newSessionID);
1660
- await state.input.client.session.promptAsync({
1661
- body: {
1662
- agent: ULTRAWORK_AGENT,
1663
- parts: [
1664
- {
1665
- type: "text",
1666
- text: `[AUTO-CYCLE] Continue processing task backlog. Read memory-bank/tasks.md and memory-bank/backlog.md, pick the next pending task, and run the full pipeline.`,
1667
- },
1668
- ],
1669
- },
1670
- path: { id: newSessionID },
1671
- });
1672
- console.log(`[opencode-immune] Multi-Cycle: Bootstrap prompt sent to ${newSessionID}`);
1744
+ pluginLog.info(`[opencode-immune] Multi-Cycle: New session created: ${newSessionID}`);
1745
+ await promptManagedSession(state, newSessionID, `[AUTO-CYCLE] Continue processing task backlog. Read memory-bank/tasks.md and memory-bank/backlog.md, pick the next pending task, and run the full pipeline.`);
1746
+ pluginLog.info(`[opencode-immune] Multi-Cycle: Bootstrap prompt sent to ${newSessionID}`);
1673
1747
  }
1674
1748
  catch (err) {
1675
- console.error("[opencode-immune] Multi-Cycle: Failed to create session or send prompt:", err);
1749
+ pluginLog.error("[opencode-immune] Multi-Cycle: Failed to create session or send prompt:", err);
1676
1750
  }
1677
1751
  }
1678
1752
  };
@@ -1705,7 +1779,7 @@ function createMultiCycleHandler(state) {
1705
1779
  RATE_LIMIT_MESSAGE_PATTERN.test(messageContent)) {
1706
1780
  if (managedSession && !managedSession.fallbackModel) {
1707
1781
  await setSessionFallbackModel(state, sessionID, RATE_LIMIT_FALLBACK_MODEL);
1708
- console.log(`[opencode-immune] Rate limit message detected in chat output for session ${sessionID}. ` +
1782
+ pluginLog.info(`[opencode-immune] Rate limit message detected in chat output for session ${sessionID}. ` +
1709
1783
  `Fallback model pinned to ${RATE_LIMIT_FALLBACK_MODEL.providerID}/${RATE_LIMIT_FALLBACK_MODEL.modelID}.`);
1710
1784
  }
1711
1785
  if (managedSession) {
@@ -1719,6 +1793,19 @@ function createMultiCycleHandler(state) {
1719
1793
  }
1720
1794
  };
1721
1795
  }
1796
+ function createPermissionAskHandler(state) {
1797
+ return async (input, output) => {
1798
+ const sessionID = input.sessionID;
1799
+ if (!isManagedUltraworkSession(state, sessionID))
1800
+ return;
1801
+ output.status = "allow";
1802
+ await writeDiagnosticLog(state, "permission:auto-allow", {
1803
+ sessionID,
1804
+ permission: input.permission,
1805
+ patterns: input.patterns,
1806
+ });
1807
+ };
1808
+ }
1722
1809
  // ═══════════════════════════════════════════════════════════════════════════════
1723
1810
  // PLUGIN MODULE EXPORT
1724
1811
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -1730,7 +1817,7 @@ async function server(input) {
1730
1817
  // Runs in background so it doesn't delay plugin initialization.
1731
1818
  // If sync fails, plugin continues normally with existing config.
1732
1819
  syncHarness(state).catch((err) => {
1733
- console.warn(`[opencode-immune] Harness sync background error:`, err instanceof Error ? err.message : String(err));
1820
+ pluginLog.warn(`[opencode-immune] Harness sync background error:`, err instanceof Error ? err.message : String(err));
1734
1821
  });
1735
1822
  // Eagerly load recovery context at plugin init so it's available
1736
1823
  // for the very first system.transform call (before chat.params fires).
@@ -1741,42 +1828,24 @@ async function server(input) {
1741
1828
  // Active task exists with incomplete phases — resume it
1742
1829
  state.recoveryContext = recovery;
1743
1830
  state.autoResumeAttempted = true;
1744
- console.log(`[opencode-immune] Plugin init: ultrawork marker active, recovery context loaded: ` +
1831
+ pluginLog.info(`[opencode-immune] Plugin init: ultrawork marker active, recovery context loaded: ` +
1745
1832
  `task="${recovery.task}", level=${recovery.level}, phase=${recovery.phase}. ` +
1746
1833
  `Will create new session and send AUTO-RESUME.`);
1747
1834
  // Create a new session and send AUTO-RESUME prompt (same pattern as CYCLE_COMPLETE).
1748
1835
  // Delay to let opencode fully initialize.
1749
1836
  setTimeout(async () => {
1750
1837
  try {
1751
- const createResult = await state.input.client.session.create({
1752
- body: {
1753
- title: `AUTO-RESUME: ${recovery.task}`,
1754
- },
1755
- });
1756
- const newSessionData = createResult?.data;
1757
- const newSessionID = newSessionData?.id;
1838
+ const newSessionID = await createManagedUltraworkSession(state, `AUTO-RESUME: ${recovery.task}`);
1758
1839
  if (!newSessionID) {
1759
- console.error("[opencode-immune] Auto-resume: Failed to create session — no session ID returned.");
1840
+ pluginLog.error("[opencode-immune] Auto-resume: Failed to create session — no session ID returned.");
1760
1841
  return;
1761
1842
  }
1762
- console.log(`[opencode-immune] Auto-resume: New session created: ${newSessionID}`);
1763
- await addManagedUltraworkSession(state, newSessionID);
1764
- await state.input.client.session.promptAsync({
1765
- body: {
1766
- agent: ULTRAWORK_AGENT,
1767
- parts: [
1768
- {
1769
- type: "text",
1770
- text: `[AUTO-RESUME] Previous session was interrupted. Read memory-bank/tasks.md, check the Phase Status block, and continue the pipeline. Use ONLY the Phase Status block to determine the next phase. Do NOT analyze or evaluate the content of tasks.md. Call the appropriate router with the exact neutral prompt from your Step 5 table.`,
1771
- },
1772
- ],
1773
- },
1774
- path: { id: newSessionID },
1775
- });
1776
- console.log(`[opencode-immune] Auto-resume prompt sent to new session ${newSessionID}`);
1843
+ pluginLog.info(`[opencode-immune] Auto-resume: New session created: ${newSessionID}`);
1844
+ await promptManagedSession(state, newSessionID, `[AUTO-RESUME] Previous session was interrupted. Read memory-bank/tasks.md, check the Phase Status block, and continue the pipeline. Use ONLY the Phase Status block to determine the next phase. Do NOT analyze or evaluate the content of tasks.md. Call the appropriate router with the exact neutral prompt from your Step 5 table.`);
1845
+ pluginLog.info(`[opencode-immune] Auto-resume prompt sent to new session ${newSessionID}`);
1777
1846
  }
1778
1847
  catch (err) {
1779
- console.error("[opencode-immune] Auto-resume: Failed to create session or send prompt:", err);
1848
+ pluginLog.error("[opencode-immune] Auto-resume: Failed to create session or send prompt:", err);
1780
1849
  }
1781
1850
  }, 5_000);
1782
1851
  }
@@ -1788,56 +1857,38 @@ async function server(input) {
1788
1857
  const hasPendingTasks = /- \[ \]/.test(backlogContent);
1789
1858
  if (hasPendingTasks) {
1790
1859
  state.autoResumeAttempted = true;
1791
- console.log(`[opencode-immune] Plugin init: no active task but backlog has pending items. ` +
1860
+ pluginLog.info(`[opencode-immune] Plugin init: no active task but backlog has pending items. ` +
1792
1861
  `Will create new session to start next cycle.`);
1793
1862
  setTimeout(async () => {
1794
1863
  try {
1795
- const createResult = await state.input.client.session.create({
1796
- body: {
1797
- title: `AUTO-CYCLE: next backlog task`,
1798
- },
1799
- });
1800
- const newSessionData = createResult?.data;
1801
- const newSessionID = newSessionData?.id;
1864
+ const newSessionID = await createManagedUltraworkSession(state, `AUTO-CYCLE: next backlog task`);
1802
1865
  if (!newSessionID) {
1803
- console.error("[opencode-immune] Auto-cycle: Failed to create session — no session ID returned.");
1866
+ pluginLog.error("[opencode-immune] Auto-cycle: Failed to create session — no session ID returned.");
1804
1867
  return;
1805
1868
  }
1806
- console.log(`[opencode-immune] Auto-cycle: New session created: ${newSessionID}`);
1807
- await addManagedUltraworkSession(state, newSessionID);
1808
- await state.input.client.session.promptAsync({
1809
- body: {
1810
- agent: ULTRAWORK_AGENT,
1811
- parts: [
1812
- {
1813
- type: "text",
1814
- text: `[AUTO-CYCLE] Continue processing task backlog. Read memory-bank/tasks.md and memory-bank/backlog.md, pick the next pending task, and run the full pipeline.`,
1815
- },
1816
- ],
1817
- },
1818
- path: { id: newSessionID },
1819
- });
1820
- console.log(`[opencode-immune] Auto-cycle prompt sent to new session ${newSessionID}`);
1869
+ pluginLog.info(`[opencode-immune] Auto-cycle: New session created: ${newSessionID}`);
1870
+ await promptManagedSession(state, newSessionID, `[AUTO-CYCLE] Continue processing task backlog. Read memory-bank/tasks.md and memory-bank/backlog.md, pick the next pending task, and run the full pipeline.`);
1871
+ pluginLog.info(`[opencode-immune] Auto-cycle prompt sent to new session ${newSessionID}`);
1821
1872
  }
1822
1873
  catch (err) {
1823
- console.error("[opencode-immune] Auto-cycle: Failed to create session or send prompt:", err);
1874
+ pluginLog.error("[opencode-immune] Auto-cycle: Failed to create session or send prompt:", err);
1824
1875
  }
1825
1876
  }, 5_000);
1826
1877
  }
1827
1878
  else {
1828
1879
  // No active task and no pending backlog — clear marker
1829
1880
  await clearUltraworkMarker(state);
1830
- console.log(`[opencode-immune] Plugin init: no active task and no pending backlog. Marker cleared.`);
1881
+ pluginLog.info(`[opencode-immune] Plugin init: no active task and no pending backlog. Marker cleared.`);
1831
1882
  }
1832
1883
  }
1833
1884
  catch {
1834
1885
  // backlog.md doesn't exist or can't be read — clear marker
1835
1886
  await clearUltraworkMarker(state);
1836
- console.log(`[opencode-immune] Plugin init: no active task, backlog unreadable. Marker cleared.`);
1887
+ pluginLog.info(`[opencode-immune] Plugin init: no active task, backlog unreadable. Marker cleared.`);
1837
1888
  }
1838
1889
  }
1839
1890
  }
1840
- console.log(`[opencode-immune] Plugin initialized. Directory: ${input.directory}`);
1891
+ pluginLog.info(`[opencode-immune] Plugin initialized. Directory: ${input.directory}`);
1841
1892
  // Compose tool.execute.after handlers:
1842
1893
  // Todo Enforcer (counter) + Ralph Loop (edit error) + Comment Checker
1843
1894
  const toolAfterHandlers = [
@@ -1854,13 +1905,14 @@ async function server(input) {
1854
1905
  createMultiCycleHandler(state),
1855
1906
  ];
1856
1907
  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)),
1908
+ event: withErrorBoundary(state, "event", createEventHandler(state)),
1909
+ "chat.message": withErrorBoundary(state, "chat.message", compositeChatMessage(chatMessageHandlers)),
1910
+ "chat.params": withErrorBoundary(state, "chat.params", createFallbackModels(state)),
1911
+ "tool.execute.after": withErrorBoundary(state, "tool.execute.after", compositeToolAfter(toolAfterHandlers)),
1912
+ "experimental.chat.system.transform": withErrorBoundary(state, "experimental.chat.system.transform", createSystemTransform(state)),
1913
+ "experimental.session.compacting": withErrorBoundary(state, "experimental.session.compacting", createCompactionHandler(state)),
1914
+ "experimental.text.complete": withErrorBoundary(state, "experimental.text.complete", createTextCompleteHandler(state)),
1915
+ "permission.ask": withErrorBoundary(state, "permission.ask", createPermissionAskHandler(state)),
1864
1916
  };
1865
1917
  }
1866
1918
  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.47",
4
4
  "description": "OpenCode plugin: session recovery, auto-retry, multi-cycle automation, context monitoring",
5
5
  "exports": {
6
6
  "./server": "./dist/plugin.js"
@@ -14,7 +14,8 @@
14
14
  "prepublishOnly": "npm run build"
15
15
  },
16
16
  "dependencies": {
17
- "@opencode-ai/plugin": "1.4.7"
17
+ "@opencode-ai/plugin": "1.14.25",
18
+ "@opencode-ai/sdk": "1.14.25"
18
19
  },
19
20
  "devDependencies": {
20
21
  "@types/node": "^25.5.2",