@khalilgharbaoui/opencode-claude-code-plugin 0.35.1 → 0.36.1
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/LICENSE +21 -0
- package/README.md +74 -1688
- package/dist/index.d.ts +33 -0
- package/dist/index.js +261 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/skills/claude-code-plugin/SKILL.md +5 -1
package/dist/index.d.ts
CHANGED
|
@@ -420,6 +420,8 @@ interface ClaudeCodeConfig {
|
|
|
420
420
|
bridgeSkipNativeSkills?: boolean;
|
|
421
421
|
/** Append a one-line cost / duration / cache footer to each finished turn. */
|
|
422
422
|
turnStats?: boolean;
|
|
423
|
+
/** Branch a forked opencode session off the parent's Claude conversation. */
|
|
424
|
+
forkSessions?: boolean;
|
|
423
425
|
logging?: LoggingConfig;
|
|
424
426
|
}
|
|
425
427
|
interface LoggingConfig {
|
|
@@ -761,6 +763,37 @@ interface ClaudeCodeProviderSettings {
|
|
|
761
763
|
* `permission_denials` always reach `providerMetadata`.
|
|
762
764
|
*/
|
|
763
765
|
turnStats?: boolean;
|
|
766
|
+
/**
|
|
767
|
+
* When a new opencode session turns out to be a fork of one this provider
|
|
768
|
+
* already served, branch the parent's Claude conversation with
|
|
769
|
+
* `claude --resume <parent> --fork-session` instead of re-rendering the
|
|
770
|
+
* whole thread as text into the first message.
|
|
771
|
+
*
|
|
772
|
+
* Measured on Claude Code 2.1.280 with haiku 4.5 over a ~13k-token
|
|
773
|
+
* conversation: the forked turn wrote 814 cache tokens and read 39,710,
|
|
774
|
+
* against 22,355 written and 17,385 read for the replay of the same thread,
|
|
775
|
+
* which is $0.0058 against $0.0467 for that one turn. The parent's own
|
|
776
|
+
* transcript is byte-identical afterwards (checked by hash).
|
|
777
|
+
*
|
|
778
|
+
* Off by default, and the reason is measured rather than cautious: a
|
|
779
|
+
* resumed Claude conversation reuses the system prompt recorded on its
|
|
780
|
+
* FIRST request (`--system-prompt-snapshot`, default `on`), so a forked
|
|
781
|
+
* session is answered under the parent's appended system prompt, not this
|
|
782
|
+
* turn's. Probed directly: a parent seeded with "your codename is ZEBRA"
|
|
783
|
+
* and forked while passing "your codename is QUAIL" answered ZEBRA. The
|
|
784
|
+
* conversation the model sees is otherwise the real transcript rather than
|
|
785
|
+
* the replay's clipped rendering, which is strictly more faithful and
|
|
786
|
+
* strictly cheaper, so this is worth turning on for branch-heavy work.
|
|
787
|
+
*
|
|
788
|
+
* It is also conservative about when it fires. Everything below keeps
|
|
789
|
+
* today's replay, unchanged: a different account, a parent whose Claude
|
|
790
|
+
* session id is unknown or gone, a parent that is still busy, a fork cut
|
|
791
|
+
* mid-conversation or taken mid tool round trip, a different cwd, model,
|
|
792
|
+
* agent, effort or prompt-cache TTL, a compaction turn, the interactive
|
|
793
|
+
* transport, an account-failover switch, and a `claude` with no
|
|
794
|
+
* `--fork-session` flag.
|
|
795
|
+
*/
|
|
796
|
+
forkSessions?: boolean;
|
|
764
797
|
/**
|
|
765
798
|
* Routing for Claude's built-in `WebSearch` tool.
|
|
766
799
|
*
|
package/dist/index.js
CHANGED
|
@@ -3931,6 +3931,190 @@ async function cancelBackgroundTask(input, options) {
|
|
|
3931
3931
|
};
|
|
3932
3932
|
}
|
|
3933
3933
|
|
|
3934
|
+
// src/session-fork.ts
|
|
3935
|
+
import { createHash as createHash3 } from "crypto";
|
|
3936
|
+
var fingerprints = /* @__PURE__ */ new Map();
|
|
3937
|
+
var fingerprintSeq = 0;
|
|
3938
|
+
var MAX_FORK_FINGERPRINTS = 64;
|
|
3939
|
+
function hashPart(hash, part) {
|
|
3940
|
+
const p = part;
|
|
3941
|
+
const type = typeof p?.type === "string" ? p.type : "unknown";
|
|
3942
|
+
switch (type) {
|
|
3943
|
+
case "text":
|
|
3944
|
+
hash.update("text\0");
|
|
3945
|
+
hash.update(String(p?.text ?? ""));
|
|
3946
|
+
break;
|
|
3947
|
+
case "tool-call":
|
|
3948
|
+
hash.update("call\0");
|
|
3949
|
+
hash.update(String(p?.toolName ?? ""));
|
|
3950
|
+
hash.update("\0");
|
|
3951
|
+
hash.update(stableJson(p?.input));
|
|
3952
|
+
break;
|
|
3953
|
+
case "tool-result":
|
|
3954
|
+
hash.update("result\0");
|
|
3955
|
+
hash.update(String(p?.toolName ?? ""));
|
|
3956
|
+
hash.update("\0");
|
|
3957
|
+
hash.update(stableJson(p?.output));
|
|
3958
|
+
break;
|
|
3959
|
+
case "file":
|
|
3960
|
+
case "image": {
|
|
3961
|
+
hash.update(type);
|
|
3962
|
+
hash.update("\0");
|
|
3963
|
+
hash.update(String(p?.mediaType ?? ""));
|
|
3964
|
+
hash.update("\0");
|
|
3965
|
+
const data = p?.data ?? p?.image;
|
|
3966
|
+
if (typeof data === "string") hash.update(data);
|
|
3967
|
+
else if (data instanceof Uint8Array) hash.update(data);
|
|
3968
|
+
else if (data instanceof URL) hash.update(data.href);
|
|
3969
|
+
else hash.update(stableJson(data));
|
|
3970
|
+
break;
|
|
3971
|
+
}
|
|
3972
|
+
default:
|
|
3973
|
+
hash.update("other\0");
|
|
3974
|
+
hash.update(type);
|
|
3975
|
+
break;
|
|
3976
|
+
}
|
|
3977
|
+
hash.update("");
|
|
3978
|
+
}
|
|
3979
|
+
function stableJson(value) {
|
|
3980
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
3981
|
+
const walk = (node) => {
|
|
3982
|
+
if (node === null || typeof node !== "object") return node;
|
|
3983
|
+
if (seen.has(node)) return "[circular]";
|
|
3984
|
+
seen.add(node);
|
|
3985
|
+
if (Array.isArray(node)) return node.map(walk);
|
|
3986
|
+
const out = {};
|
|
3987
|
+
for (const key of Object.keys(node).sort()) {
|
|
3988
|
+
out[key] = walk(node[key]);
|
|
3989
|
+
}
|
|
3990
|
+
return out;
|
|
3991
|
+
};
|
|
3992
|
+
try {
|
|
3993
|
+
return JSON.stringify(walk(value)) ?? "null";
|
|
3994
|
+
} catch {
|
|
3995
|
+
return "[unserializable]";
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
function messageDigest(msg) {
|
|
3999
|
+
const hash = createHash3("sha256");
|
|
4000
|
+
hash.update(msg.role);
|
|
4001
|
+
hash.update("\0");
|
|
4002
|
+
if (typeof msg.content === "string") {
|
|
4003
|
+
hash.update("text\0");
|
|
4004
|
+
hash.update(msg.content);
|
|
4005
|
+
hash.update("");
|
|
4006
|
+
} else if (Array.isArray(msg.content)) {
|
|
4007
|
+
for (const part of msg.content) hashPart(hash, part);
|
|
4008
|
+
}
|
|
4009
|
+
return hash.digest("hex").slice(0, 32);
|
|
4010
|
+
}
|
|
4011
|
+
function conversationDigests(prompt) {
|
|
4012
|
+
const out = [];
|
|
4013
|
+
for (const msg of prompt) {
|
|
4014
|
+
if (msg.role !== "user" && msg.role !== "assistant" && msg.role !== "tool") {
|
|
4015
|
+
continue;
|
|
4016
|
+
}
|
|
4017
|
+
out.push({
|
|
4018
|
+
role: msg.role,
|
|
4019
|
+
digest: messageDigest(msg)
|
|
4020
|
+
});
|
|
4021
|
+
}
|
|
4022
|
+
return out;
|
|
4023
|
+
}
|
|
4024
|
+
function recordForkFingerprint(sessionKey2, prompt, cliPath) {
|
|
4025
|
+
const entries = conversationDigests(prompt);
|
|
4026
|
+
if (entries.length === 0) return;
|
|
4027
|
+
fingerprints.delete(sessionKey2);
|
|
4028
|
+
fingerprints.set(sessionKey2, { entries, cliPath, seq: ++fingerprintSeq });
|
|
4029
|
+
while (fingerprints.size > MAX_FORK_FINGERPRINTS) {
|
|
4030
|
+
const oldest = fingerprints.keys().next();
|
|
4031
|
+
if (oldest.done) break;
|
|
4032
|
+
fingerprints.delete(oldest.value);
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
function forgetForkFingerprint(sessionKey2) {
|
|
4036
|
+
fingerprints.delete(sessionKey2);
|
|
4037
|
+
}
|
|
4038
|
+
function forkSiblingSignature(sessionKey2) {
|
|
4039
|
+
const parts = sessionKey2.split("::");
|
|
4040
|
+
if (parts.length < 5) return null;
|
|
4041
|
+
if (parts[2] === "compaction") return null;
|
|
4042
|
+
const blanked = [...parts];
|
|
4043
|
+
blanked[3] = "*";
|
|
4044
|
+
return blanked.join("::");
|
|
4045
|
+
}
|
|
4046
|
+
function splitForkHistory(prompt) {
|
|
4047
|
+
const messages = prompt;
|
|
4048
|
+
let lastAssistant = -1;
|
|
4049
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
4050
|
+
if (messages[i].role === "assistant") {
|
|
4051
|
+
lastAssistant = i;
|
|
4052
|
+
break;
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
if (lastAssistant < 0) return { history: [], forkable: false };
|
|
4056
|
+
for (let i = lastAssistant + 1; i < messages.length; i++) {
|
|
4057
|
+
const msg = messages[i];
|
|
4058
|
+
if (msg.role === "tool") return { history: [], forkable: false };
|
|
4059
|
+
if (!Array.isArray(msg.content)) continue;
|
|
4060
|
+
for (const part of msg.content) {
|
|
4061
|
+
if (part?.type === "tool-result") return { history: [], forkable: false };
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
return {
|
|
4065
|
+
history: conversationDigests(messages.slice(0, lastAssistant + 1)),
|
|
4066
|
+
forkable: true
|
|
4067
|
+
};
|
|
4068
|
+
}
|
|
4069
|
+
function findForkParent(opts) {
|
|
4070
|
+
const signature = forkSiblingSignature(opts.sessionKey);
|
|
4071
|
+
if (!signature) return null;
|
|
4072
|
+
const { history, forkable } = splitForkHistory(opts.prompt);
|
|
4073
|
+
if (!forkable || history.length === 0) return null;
|
|
4074
|
+
let best = null;
|
|
4075
|
+
let bestLength = 0;
|
|
4076
|
+
let bestSeq = -1;
|
|
4077
|
+
for (const [key, fingerprint] of fingerprints) {
|
|
4078
|
+
if (key === opts.sessionKey) continue;
|
|
4079
|
+
if (fingerprint.cliPath !== opts.cliPath) continue;
|
|
4080
|
+
if (forkSiblingSignature(key) !== signature) continue;
|
|
4081
|
+
const recorded = fingerprint.entries;
|
|
4082
|
+
if (recorded.length === 0 || recorded.length > history.length) continue;
|
|
4083
|
+
if (!isPrefix(recorded, history)) continue;
|
|
4084
|
+
if (!tailIsReplyOnly(history, recorded.length)) continue;
|
|
4085
|
+
const claudeSessionId = opts.lookupClaudeSessionId(key);
|
|
4086
|
+
if (!claudeSessionId) continue;
|
|
4087
|
+
if (opts.isBusy(key)) continue;
|
|
4088
|
+
if (recorded.length > bestLength || recorded.length === bestLength && fingerprint.seq > bestSeq) {
|
|
4089
|
+
best = { parentKey: key, claudeSessionId, matched: recorded.length };
|
|
4090
|
+
bestLength = recorded.length;
|
|
4091
|
+
bestSeq = fingerprint.seq;
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
if (best) {
|
|
4095
|
+
log.info("found a fork parent for this conversation", {
|
|
4096
|
+
sessionKey: opts.sessionKey,
|
|
4097
|
+
parentKey: best.parentKey,
|
|
4098
|
+
matchedMessages: best.matched,
|
|
4099
|
+
historyMessages: history.length
|
|
4100
|
+
});
|
|
4101
|
+
}
|
|
4102
|
+
return best;
|
|
4103
|
+
}
|
|
4104
|
+
function isPrefix(recorded, history) {
|
|
4105
|
+
for (let i = 0; i < recorded.length; i++) {
|
|
4106
|
+
if (recorded[i].role !== history[i].role) return false;
|
|
4107
|
+
if (recorded[i].digest !== history[i].digest) return false;
|
|
4108
|
+
}
|
|
4109
|
+
return true;
|
|
4110
|
+
}
|
|
4111
|
+
function tailIsReplyOnly(history, from) {
|
|
4112
|
+
for (let i = from; i < history.length; i++) {
|
|
4113
|
+
if (history[i].role === "user") return false;
|
|
4114
|
+
}
|
|
4115
|
+
return true;
|
|
4116
|
+
}
|
|
4117
|
+
|
|
3934
4118
|
// src/permission-presets.ts
|
|
3935
4119
|
var READ_ONLY_DISALLOWED_CLI_TOOLS = [
|
|
3936
4120
|
"Bash",
|
|
@@ -4535,6 +4719,11 @@ function claudeSessionIsBusy(key) {
|
|
|
4535
4719
|
if (activeProcesses.has(key)) return true;
|
|
4536
4720
|
return getPendingProxyCalls(key).length > 0 || hasExitPlanModeQuestions(key);
|
|
4537
4721
|
}
|
|
4722
|
+
function claudeSessionIsWriting(key) {
|
|
4723
|
+
const active = activeProcesses.get(key);
|
|
4724
|
+
if (active && isTurnInFlight(active)) return true;
|
|
4725
|
+
return getPendingProxyCalls(key).length > 0 || hasExitPlanModeQuestions(key);
|
|
4726
|
+
}
|
|
4538
4727
|
function capClaudeSessions() {
|
|
4539
4728
|
for (const key of [...claudeSessions.keys()]) {
|
|
4540
4729
|
if (claudeSessions.size <= MAX_CLAUDE_SESSION_ENTRIES) return;
|
|
@@ -4558,6 +4747,7 @@ function deleteClaudeSessionId(key) {
|
|
|
4558
4747
|
const claudeSessionId = claudeSessions.get(key);
|
|
4559
4748
|
if (claudeSessionId) clearLedger(claudeSessionId);
|
|
4560
4749
|
claudeSessions.delete(key);
|
|
4750
|
+
forgetForkFingerprint(key);
|
|
4561
4751
|
}
|
|
4562
4752
|
function effortSessionKey(baseKey, effort) {
|
|
4563
4753
|
return effort ? `${baseKey}::effort=${effort}` : baseKey;
|
|
@@ -4694,6 +4884,25 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
4694
4884
|
return ap;
|
|
4695
4885
|
}
|
|
4696
4886
|
function appendResumeIfNeeded(sessionKey2, cliArgs) {
|
|
4887
|
+
if (cliArgs.includes("--fork-session")) {
|
|
4888
|
+
const forkedSessionId = claudeSessions.get(sessionKey2);
|
|
4889
|
+
if (!forkedSessionId) return cliArgs;
|
|
4890
|
+
const resumed = [];
|
|
4891
|
+
for (let i = 0; i < cliArgs.length; i++) {
|
|
4892
|
+
if (cliArgs[i] === "--fork-session") continue;
|
|
4893
|
+
if (cliArgs[i] === "--resume") {
|
|
4894
|
+
resumed.push("--resume", forkedSessionId);
|
|
4895
|
+
i++;
|
|
4896
|
+
continue;
|
|
4897
|
+
}
|
|
4898
|
+
resumed.push(cliArgs[i]);
|
|
4899
|
+
}
|
|
4900
|
+
log.info("respawning a forked session on its own claude conversation", {
|
|
4901
|
+
sessionKey: sessionKey2,
|
|
4902
|
+
claudeSessionId: forkedSessionId
|
|
4903
|
+
});
|
|
4904
|
+
return resumed;
|
|
4905
|
+
}
|
|
4697
4906
|
if (cliArgs.includes("--resume") || cliArgs.includes("--session-id")) {
|
|
4698
4907
|
return cliArgs;
|
|
4699
4908
|
}
|
|
@@ -4734,6 +4943,7 @@ function buildCliArgs(opts) {
|
|
|
4734
4943
|
sessionKey: sessionKey2,
|
|
4735
4944
|
skipPermissions,
|
|
4736
4945
|
includeSessionId = true,
|
|
4946
|
+
forkFromClaudeSessionId,
|
|
4737
4947
|
model,
|
|
4738
4948
|
permissionMode,
|
|
4739
4949
|
mcpConfig,
|
|
@@ -4783,6 +4993,8 @@ function buildCliArgs(opts) {
|
|
|
4783
4993
|
const sessionId = claudeSessions.get(sessionKey2);
|
|
4784
4994
|
if (sessionId && !activeProcesses.has(sessionKey2)) {
|
|
4785
4995
|
args.push("--resume", sessionId);
|
|
4996
|
+
} else if (forkFromClaudeSessionId) {
|
|
4997
|
+
args.push("--resume", forkFromClaudeSessionId, "--fork-session");
|
|
4786
4998
|
}
|
|
4787
4999
|
}
|
|
4788
5000
|
if (mcpConfig) {
|
|
@@ -5138,7 +5350,7 @@ async function handleBtwCommand(client, input, options = {}) {
|
|
|
5138
5350
|
}
|
|
5139
5351
|
|
|
5140
5352
|
// src/diagnostic-bundle.ts
|
|
5141
|
-
import { createHash as
|
|
5353
|
+
import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
|
|
5142
5354
|
import { openSync, readSync, closeSync, fstatSync, realpathSync } from "fs";
|
|
5143
5355
|
import { homedir as homedir4 } from "os";
|
|
5144
5356
|
|
|
@@ -5208,6 +5420,8 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5208
5420
|
"falling back to the next model in the chain",
|
|
5209
5421
|
"fast mode is in cooldown after a rate limit; this turn runs at standard speed and is billed at standard Opus rates, not the fast price shown in the model picker.",
|
|
5210
5422
|
"file part without data, skipping",
|
|
5423
|
+
"forkSessions is on but this claude CLI has no --fork-session; replaying the conversation instead",
|
|
5424
|
+
"forking the parent conversation's claude session instead of replaying it",
|
|
5211
5425
|
"ignoring a failover reset time that is not in the future",
|
|
5212
5426
|
"ignoring unknown proxyOpencodeTools entries",
|
|
5213
5427
|
"ignoring unknown proxyTools entries",
|
|
@@ -5459,7 +5673,7 @@ function shape(value) {
|
|
|
5459
5673
|
return `[redacted, ${size} chars]`;
|
|
5460
5674
|
}
|
|
5461
5675
|
function createIdHasher(salt = randomBytes2(16).toString("hex")) {
|
|
5462
|
-
return (raw) =>
|
|
5676
|
+
return (raw) => createHash4("sha256").update(salt).update("\0").update(raw).digest("hex").slice(0, 8);
|
|
5463
5677
|
}
|
|
5464
5678
|
function createRedactionContext(options = {}) {
|
|
5465
5679
|
const home = options.home ?? homedir4();
|
|
@@ -11507,6 +11721,49 @@ var ClaudeCodeLanguageModel = class {
|
|
|
11507
11721
|
if (failoverAnswer?.kind === "switch" && hasPriorConversation) {
|
|
11508
11722
|
includeHistoryContext = true;
|
|
11509
11723
|
}
|
|
11724
|
+
let forkFromClaudeSessionId;
|
|
11725
|
+
if (this.config.forkSessions === true && includeHistoryContext && !compactionMode && !useInteractive && failoverAnswer?.kind !== "switch") {
|
|
11726
|
+
const parent = findForkParent({
|
|
11727
|
+
sessionKey: sk,
|
|
11728
|
+
prompt: options.prompt,
|
|
11729
|
+
cliPath,
|
|
11730
|
+
lookupClaudeSessionId: getClaudeSessionId,
|
|
11731
|
+
// `claudeSessionIsWriting`, not `getActiveProcess`: this walks every
|
|
11732
|
+
// sibling key, and that one refreshes LRU order and cancels idle
|
|
11733
|
+
// timers on everything it touches. An idle live child is fine to
|
|
11734
|
+
// branch from; a transcript mid-write is not.
|
|
11735
|
+
isBusy: claudeSessionIsWriting
|
|
11736
|
+
});
|
|
11737
|
+
if (parent) {
|
|
11738
|
+
const supported = await Promise.race([
|
|
11739
|
+
detectCliSupportsFlag(cliPath, "--fork-session"),
|
|
11740
|
+
turnAbort.whenAborted.then(() => false)
|
|
11741
|
+
]);
|
|
11742
|
+
if (turnAbort.aborted) {
|
|
11743
|
+
return abortedBeforeWork("probing --fork-session support");
|
|
11744
|
+
}
|
|
11745
|
+
if (supported) {
|
|
11746
|
+
forkFromClaudeSessionId = parent.claudeSessionId;
|
|
11747
|
+
includeHistoryContext = false;
|
|
11748
|
+
log.notice(
|
|
11749
|
+
"forking the parent conversation's claude session instead of replaying it",
|
|
11750
|
+
{
|
|
11751
|
+
sessionKey: sk,
|
|
11752
|
+
parentKey: parent.parentKey,
|
|
11753
|
+
matchedMessages: parent.matched
|
|
11754
|
+
}
|
|
11755
|
+
);
|
|
11756
|
+
} else {
|
|
11757
|
+
log.warn(
|
|
11758
|
+
"forkSessions is on but this claude CLI has no --fork-session; replaying the conversation instead",
|
|
11759
|
+
{ cliPath }
|
|
11760
|
+
);
|
|
11761
|
+
}
|
|
11762
|
+
}
|
|
11763
|
+
}
|
|
11764
|
+
if (this.config.forkSessions === true && !compactionMode && !useInteractive) {
|
|
11765
|
+
recordForkFingerprint(sk, options.prompt, cliPath);
|
|
11766
|
+
}
|
|
11510
11767
|
const { model: spawnModelId, fast: fastMode } = parseModelId(failover.modelId);
|
|
11511
11768
|
const exitPlanModeQuestionResult = compactionMode ? null : consumeExitPlanModeQuestionResult(sk, options.prompt);
|
|
11512
11769
|
if (exitPlanModeQuestionResult) {
|
|
@@ -11981,6 +12238,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
11981
12238
|
state.cliArgs = buildCliArgs({
|
|
11982
12239
|
sessionKey: sk,
|
|
11983
12240
|
skipPermissions,
|
|
12241
|
+
forkFromClaudeSessionId,
|
|
11984
12242
|
model: spawnModelId,
|
|
11985
12243
|
permissionMode: self.config.permissionMode,
|
|
11986
12244
|
mcpConfig: mcp.paths,
|
|
@@ -13211,6 +13469,7 @@ function createClaudeCode(settings = {}) {
|
|
|
13211
13469
|
bridgeOpencodeSkills: settings.bridgeOpencodeSkills === true,
|
|
13212
13470
|
bridgeSkipNativeSkills: settings.bridgeSkipNativeSkills !== false,
|
|
13213
13471
|
turnStats: settings.turnStats === true,
|
|
13472
|
+
forkSessions: settings.forkSessions === true,
|
|
13214
13473
|
interactive: settings.interactive,
|
|
13215
13474
|
interactiveBypass: settings.interactiveBypass,
|
|
13216
13475
|
interactiveAllowTools: settings.interactiveAllowTools,
|