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.
- package/dist/plugin/server.js +175 -22
- package/package.json +1 -1
package/dist/plugin/server.js
CHANGED
|
@@ -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.
|
|
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
|
|
5422
|
+
const newSessionID = await startAutoCycleInNewSession(
|
|
5274
5423
|
state,
|
|
5275
|
-
|
|
5276
|
-
|
|
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
|
|
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
|
|
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
|
|
5990
|
+
await startAutoCycleInNewSession(
|
|
5843
5991
|
state,
|
|
5844
|
-
|
|
5845
|
-
|
|
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
|
|
6023
|
+
await startAutoCycleInNewSession(
|
|
5873
6024
|
state,
|
|
5874
|
-
|
|
5875
|
-
|
|
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;
|