opencode-immune 1.0.74 → 1.0.76

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/server.js +175 -22
  2. package/package.json +1 -1
@@ -3777,7 +3777,7 @@ import { fileURLToPath } from "url";
3777
3777
  import { createHash } from "crypto";
3778
3778
  import { tmpdir } from "os";
3779
3779
  import { execFile } from "child_process";
3780
- var PLUGIN_VERSION = "1.0.74";
3780
+ var PLUGIN_VERSION = "1.0.76";
3781
3781
  var PLUGIN_PACKAGE_NAME = "opencode-immune";
3782
3782
  var PLUGIN_DIRNAME = dirname(fileURLToPath(import.meta.url));
3783
3783
  function getServerAuthHeaders() {
@@ -3948,6 +3948,42 @@ async function createManagedUltraworkSession(state, title) {
3948
3948
  await addManagedUltraworkSession(state, sessionID);
3949
3949
  return sessionID;
3950
3950
  }
3951
+ async function startAutoCycleInNewSession(state, options) {
3952
+ const nextTask = options.nextTask?.trim() || "Continue processing task backlog";
3953
+ const title = `AUTO-CYCLE: ${nextTask}`;
3954
+ state.autoResumeInFlight = true;
3955
+ try {
3956
+ const newSessionID = await createManagedUltraworkSession(state, title);
3957
+ if (!newSessionID) {
3958
+ throw new Error("session.create returned no session ID");
3959
+ }
3960
+ await refreshAutoCycleLock(state, newSessionID);
3961
+ await promptManagedSession(
3962
+ state,
3963
+ newSessionID,
3964
+ `[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.`
3965
+ );
3966
+ state.autoResumeAttempted = true;
3967
+ if (options.retireSourceSession && isManagedUltraworkSession(state, options.sourceSessionID)) {
3968
+ await removeManagedUltraworkSession(
3969
+ state,
3970
+ options.sourceSessionID,
3971
+ "auto-cycle moved to a new session"
3972
+ );
3973
+ }
3974
+ pluginLog.info(
3975
+ `[opencode-immune] ${options.logContext}: Bootstrap prompt sent to ${newSessionID}`
3976
+ );
3977
+ return newSessionID;
3978
+ } catch (err) {
3979
+ if (options.clearLockOnFailureReason) {
3980
+ await clearAutoCycleLock(state, options.clearLockOnFailureReason);
3981
+ }
3982
+ throw err;
3983
+ } finally {
3984
+ state.autoResumeInFlight = false;
3985
+ }
3986
+ }
3951
3987
  async function applyUltraworkSessionPermissions(state, sessionID) {
3952
3988
  if (state.ultraworkPermissionSessions.has(sessionID)) return;
3953
3989
  try {
@@ -5074,6 +5110,104 @@ async function fileHash(filePath) {
5074
5110
  return "";
5075
5111
  }
5076
5112
  }
5113
+ function parseImmunePluginSpec(value) {
5114
+ const trimmed = value.trim();
5115
+ const match = trimmed.match(/^opencode-immune(?:@(.+))?$/);
5116
+ if (!match) return null;
5117
+ return {
5118
+ spec: trimmed,
5119
+ version: match[1] ?? null
5120
+ };
5121
+ }
5122
+ async function readProjectImmunePluginSpec(directory) {
5123
+ try {
5124
+ const raw = await readFile(join(directory, "opencode.json"), "utf-8");
5125
+ const config = JSON.parse(raw);
5126
+ const plugins = Array.isArray(config.plugin) ? config.plugin : [];
5127
+ for (let index = plugins.length - 1; index >= 0; index--) {
5128
+ const value = plugins[index];
5129
+ if (typeof value !== "string") continue;
5130
+ const parsed = parseImmunePluginSpec(value);
5131
+ if (parsed) return parsed;
5132
+ }
5133
+ } catch {
5134
+ }
5135
+ return null;
5136
+ }
5137
+ function execFileAsync(command, args, options) {
5138
+ return new Promise((resolve, reject) => {
5139
+ execFile(command, args, options ?? {}, (err, stdout, stderr) => {
5140
+ if (err) {
5141
+ reject(err);
5142
+ return;
5143
+ }
5144
+ resolve({ stdout, stderr });
5145
+ });
5146
+ });
5147
+ }
5148
+ async function clearProjectPluginOverride(directory) {
5149
+ const overridePath = join(directory, ".opencode", "opencode.json");
5150
+ try {
5151
+ const raw = await readFile(overridePath, "utf-8");
5152
+ const config = JSON.parse(raw);
5153
+ const keys = Object.keys(config ?? {});
5154
+ const plugins = Array.isArray(config.plugin) ? config.plugin : [];
5155
+ const isGeneratedVersionPin = plugins.length === 1 && typeof plugins[0] === "string" && /^opencode-immune@.+$/.test(plugins[0]) && keys.every((key) => key === "plugin" || key === "$schema");
5156
+ if (!isGeneratedVersionPin) {
5157
+ return false;
5158
+ }
5159
+ await unlink(overridePath);
5160
+ return true;
5161
+ } catch {
5162
+ return false;
5163
+ }
5164
+ }
5165
+ async function installPinnedProjectPlugin(state, pluginSpec) {
5166
+ if (!pluginSpec.version) return false;
5167
+ const currentVersion = await getPluginVersion();
5168
+ if (pluginSpec.version === currentVersion) {
5169
+ const overrideRemoved = await clearProjectPluginOverride(state.input.directory);
5170
+ if (overrideRemoved) {
5171
+ pluginLog.info(
5172
+ `[opencode-immune] Harness sync: removed stale local plugin override after confirming ${pluginSpec.spec}.`
5173
+ );
5174
+ }
5175
+ return false;
5176
+ }
5177
+ try {
5178
+ pluginLog.info(
5179
+ `[opencode-immune] Harness sync: installing ${pluginSpec.spec} in ${state.input.directory}`
5180
+ );
5181
+ await execFileAsync("opencode", ["plugin", pluginSpec.spec, "--force"], {
5182
+ cwd: state.input.directory,
5183
+ env: process.env
5184
+ });
5185
+ const overrideRemoved = await clearProjectPluginOverride(state.input.directory);
5186
+ if (overrideRemoved) {
5187
+ pluginLog.info(
5188
+ `[opencode-immune] Harness sync: removed local plugin override after installing ${pluginSpec.spec}.`
5189
+ );
5190
+ }
5191
+ state.pluginUpdateMessage = `[PLUGIN UPDATE] Harness synced this project to ${pluginSpec.spec}. Restart opencode to load the new plugin version.`;
5192
+ await writeDiagnosticLog(state, "harness-sync:plugin-install", {
5193
+ pluginSpec: pluginSpec.spec,
5194
+ status: "installed"
5195
+ });
5196
+ return true;
5197
+ } catch (err) {
5198
+ pluginLog.warn(
5199
+ `[opencode-immune] Harness sync: automatic install failed for ${pluginSpec.spec}.`,
5200
+ err instanceof Error ? err.message : String(err)
5201
+ );
5202
+ state.pluginUpdateMessage = `[PLUGIN UPDATE] Harness synced this project to ${pluginSpec.spec}, but automatic install failed. Run \`opencode plugin ${pluginSpec.spec} --force\` in this project, then restart opencode.`;
5203
+ await writeDiagnosticLog(state, "harness-sync:plugin-install", {
5204
+ pluginSpec: pluginSpec.spec,
5205
+ status: "failed",
5206
+ error: err instanceof Error ? err.message : String(err)
5207
+ });
5208
+ return false;
5209
+ }
5210
+ }
5077
5211
  async function syncHarness(state) {
5078
5212
  const token = await resolveEnvValue(state.input.directory, HARNESS_TOKEN_ENV);
5079
5213
  if (!token) {
@@ -5124,19 +5258,36 @@ async function syncHarness(state) {
5124
5258
  release.tagName + "\n",
5125
5259
  "utf-8"
5126
5260
  );
5261
+ const projectPluginSpec = await readProjectImmunePluginSpec(state.input.directory);
5262
+ const pluginInstalled = projectPluginSpec ? await installPinnedProjectPlugin(state, projectPluginSpec) : false;
5263
+ if (!projectPluginSpec) {
5264
+ const overrideRemoved = await clearProjectPluginOverride(state.input.directory);
5265
+ if (overrideRemoved) {
5266
+ pluginLog.info(
5267
+ `[opencode-immune] Harness sync: removed local plugin override to keep project config authoritative.`
5268
+ );
5269
+ }
5270
+ }
5127
5271
  const hashAfter = await fileHash(configPath);
5128
5272
  if (hashBefore && hashAfter && hashBefore !== hashAfter) {
5129
5273
  pluginLog.warn(
5130
5274
  `[opencode-immune] \u26A0 Harness sync: opencode.json was updated. Please restart opencode for the new agent configuration to take effect.`
5131
5275
  );
5132
5276
  }
5277
+ if (pluginInstalled) {
5278
+ pluginLog.warn(
5279
+ `[opencode-immune] \u26A0 Harness sync: installed updated plugin from project config. Restart opencode to load the new plugin code.`
5280
+ );
5281
+ }
5133
5282
  pluginLog.info(
5134
5283
  `[opencode-immune] Harness sync: successfully updated to ${release.tagName}`
5135
5284
  );
5136
5285
  await writeDiagnosticLog(state, "harness-sync:success", {
5137
5286
  from: localVersion,
5138
5287
  to: release.tagName,
5139
- configChanged: hashBefore !== hashAfter
5288
+ configChanged: hashBefore !== hashAfter,
5289
+ pluginSpec: projectPluginSpec?.spec ?? null,
5290
+ pluginInstalled
5140
5291
  });
5141
5292
  } finally {
5142
5293
  try {
@@ -5265,28 +5416,25 @@ function createSessionRecoveryEvent(state) {
5265
5416
  sessionID
5266
5417
  );
5267
5418
  if (!lockAcquired) return;
5268
- await addManagedUltraworkSession(state, sessionID);
5269
5419
  await refreshAutoCycleLock(state, sessionID);
5270
- state.autoResumeInFlight = true;
5271
5420
  setTimeout(async () => {
5272
5421
  try {
5273
- await promptManagedSession(
5422
+ const newSessionID = await startAutoCycleInNewSession(
5274
5423
  state,
5275
- sessionID,
5276
- `[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.`
5424
+ {
5425
+ sourceSessionID: sessionID,
5426
+ logContext: "Auto-cycle",
5427
+ clearLockOnFailureReason: "root session auto-cycle failed"
5428
+ }
5277
5429
  );
5278
- state.autoResumeAttempted = true;
5279
5430
  pluginLog.info(
5280
- `[opencode-immune] Auto-cycle prompt sent to root ultrawork session ${sessionID}`
5431
+ `[opencode-immune] Auto-cycle prompt sent to new session ${newSessionID}`
5281
5432
  );
5282
5433
  } catch (err) {
5283
- await clearAutoCycleLock(state, "root session auto-cycle failed");
5284
5434
  pluginLog.error(
5285
5435
  `[opencode-immune] Auto-cycle prompt failed for root session ${sessionID}:`,
5286
5436
  err
5287
5437
  );
5288
- } finally {
5289
- state.autoResumeInFlight = false;
5290
5438
  }
5291
5439
  }, 3e3);
5292
5440
  }
@@ -5836,15 +5984,18 @@ function createTextCompleteHandler(state) {
5836
5984
  const taskMatch = text.match(NEXT_TASK_PATTERN);
5837
5985
  const nextTask = taskMatch?.[1]?.trim() ?? "Continue processing task backlog";
5838
5986
  pluginLog.info(
5839
- `[opencode-immune] Multi-Cycle: Starting next cycle in current session (cycle ${state.cycleCount}/${MAX_CYCLES}) for: "${nextTask}"`
5987
+ `[opencode-immune] Multi-Cycle: Starting next cycle in a new session (cycle ${state.cycleCount}/${MAX_CYCLES}) for: "${nextTask}"`
5840
5988
  );
5841
5989
  try {
5842
- await promptManagedSession(
5990
+ await startAutoCycleInNewSession(
5843
5991
  state,
5844
- sessionID,
5845
- `[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.`
5992
+ {
5993
+ sourceSessionID: sessionID,
5994
+ nextTask,
5995
+ logContext: "Multi-Cycle",
5996
+ retireSourceSession: true
5997
+ }
5846
5998
  );
5847
- pluginLog.info(`[opencode-immune] Multi-Cycle: Bootstrap prompt sent to ${sessionID}`);
5848
5999
  } catch (err) {
5849
6000
  pluginLog.error("[opencode-immune] Multi-Cycle: Failed to send prompt:", err);
5850
6001
  }
@@ -5869,15 +6020,17 @@ function createTextCompleteHandler(state) {
5869
6020
  );
5870
6021
  try {
5871
6022
  await refreshAutoCycleLock(state, sessionID);
5872
- await promptManagedSession(
6023
+ await startAutoCycleInNewSession(
5873
6024
  state,
5874
- sessionID,
5875
- `[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.`
6025
+ {
6026
+ sourceSessionID: sessionID,
6027
+ logContext: "Multi-Cycle fallback",
6028
+ clearLockOnFailureReason: "fallback bootstrap failed",
6029
+ retireSourceSession: true
6030
+ }
5876
6031
  );
5877
- pluginLog.info(`[opencode-immune] Multi-Cycle fallback: Bootstrap prompt sent to ${sessionID}`);
5878
6032
  } catch (err) {
5879
6033
  state.autoCycleSourceSessions.delete(sessionID);
5880
- await clearAutoCycleLock(state, "fallback bootstrap failed");
5881
6034
  pluginLog.error("[opencode-immune] Multi-Cycle fallback: Failed to send prompt:", err);
5882
6035
  } finally {
5883
6036
  state.autoCycleInFlight = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-immune",
3
- "version": "1.0.74",
3
+ "version": "1.0.76",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin: session recovery, auto-retry, multi-cycle automation, context monitoring",
6
6
  "exports": {