oh-my-opencode-slim 0.9.8 → 0.9.10

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/index.js CHANGED
@@ -2956,34 +2956,1439 @@ ${bestResult.result}` : undefined,
2956
2956
  };
2957
2957
  }
2958
2958
  }
2959
+ // src/hooks/apply-patch/errors.ts
2960
+ var APPLY_PATCH_ERROR_PREFIX = {
2961
+ blocked: "apply_patch blocked",
2962
+ validation: "apply_patch validation failed",
2963
+ verification: "apply_patch verification failed",
2964
+ internal: "apply_patch internal error"
2965
+ };
2966
+
2967
+ class ApplyPatchError extends Error {
2968
+ kind;
2969
+ code;
2970
+ cause;
2971
+ constructor(kind, code, message, options) {
2972
+ super(`${APPLY_PATCH_ERROR_PREFIX[kind]}: ${message}`);
2973
+ this.kind = kind;
2974
+ this.code = code;
2975
+ this.name = "ApplyPatchError";
2976
+ this.cause = options?.cause;
2977
+ }
2978
+ }
2979
+ function getErrorMessage(error) {
2980
+ return error instanceof Error ? error.message : String(error);
2981
+ }
2982
+ function createApplyPatchBlockedError(message, cause) {
2983
+ return new ApplyPatchError("blocked", "outside_workspace", message, {
2984
+ cause
2985
+ });
2986
+ }
2987
+ function createApplyPatchValidationError(message, cause) {
2988
+ return new ApplyPatchError("validation", "malformed_patch", message, {
2989
+ cause
2990
+ });
2991
+ }
2992
+ function createApplyPatchVerificationError(message, cause) {
2993
+ return new ApplyPatchError("verification", "verification_failed", message, {
2994
+ cause
2995
+ });
2996
+ }
2997
+ function createApplyPatchInternalError(message, cause) {
2998
+ return new ApplyPatchError("internal", "internal_unexpected", message, {
2999
+ cause
3000
+ });
3001
+ }
3002
+ function isApplyPatchError(error) {
3003
+ return error instanceof ApplyPatchError;
3004
+ }
3005
+ function isApplyPatchVerificationError(error) {
3006
+ return isApplyPatchError(error) && error.kind === "verification";
3007
+ }
3008
+ function getApplyPatchErrorDetails(error) {
3009
+ if (!isApplyPatchError(error)) {
3010
+ return;
3011
+ }
3012
+ return {
3013
+ kind: error.kind,
3014
+ code: error.code,
3015
+ message: error.message
3016
+ };
3017
+ }
3018
+ function ensureApplyPatchError(error, context) {
3019
+ if (isApplyPatchError(error)) {
3020
+ return error;
3021
+ }
3022
+ return createApplyPatchInternalError(`${context}: ${getErrorMessage(error)}`, error);
3023
+ }
3024
+
3025
+ // src/hooks/apply-patch/execution-context.ts
3026
+ import * as fs3 from "fs/promises";
3027
+ import path3 from "path";
3028
+
3029
+ // src/hooks/apply-patch/codec.ts
3030
+ function normalizeLineEndings(text) {
3031
+ return text.replace(/\r\n/g, `
3032
+ `).replace(/\r/g, `
3033
+ `);
3034
+ }
3035
+ function normalizeUnicode(text) {
3036
+ return text.replace(/[\u2018\u2019\u201A\u201B]/g, "'").replace(/[\u201C\u201D\u201E\u201F]/g, '"').replace(/[\u2010\u2011\u2012\u2013\u2014\u2015]/g, "-").replace(/\u2026/g, "...").replace(/\u00A0/g, " ");
3037
+ }
3038
+ function stripHeredoc(input) {
3039
+ const normalized = normalizeLineEndings(input);
3040
+ const match = normalized.match(/^(?:cat\s+)?<<['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\1\s*$/);
3041
+ return match ? match[2] : normalized;
3042
+ }
3043
+ function normalizePatchText(patchText) {
3044
+ return stripHeredoc(normalizeLineEndings(patchText).trim());
3045
+ }
3046
+ function parseHeader(lines, index) {
3047
+ const line = lines[index];
3048
+ if (line.startsWith("*** Add File:")) {
3049
+ const file = line.slice("*** Add File:".length).trim();
3050
+ return file ? { file, next: index + 1 } : null;
3051
+ }
3052
+ if (line.startsWith("*** Delete File:")) {
3053
+ const file = line.slice("*** Delete File:".length).trim();
3054
+ return file ? { file, next: index + 1 } : null;
3055
+ }
3056
+ if (line.startsWith("*** Update File:")) {
3057
+ const file = line.slice("*** Update File:".length).trim();
3058
+ let move;
3059
+ let next = index + 1;
3060
+ if (next < lines.length && lines[next].startsWith("*** Move to:")) {
3061
+ const moveTarget = lines[next].slice("*** Move to:".length).trim();
3062
+ if (!moveTarget) {
3063
+ return null;
3064
+ }
3065
+ move = moveTarget;
3066
+ next += 1;
3067
+ }
3068
+ return file ? { file, move, next } : null;
3069
+ }
3070
+ return null;
3071
+ }
3072
+ function unexpectedPatchLine(context, line) {
3073
+ const rendered = line.length === 0 ? "<empty>" : line;
3074
+ throw new Error(`Invalid patch format: unexpected line ${context}: ${rendered}`);
3075
+ }
3076
+ function parseChunks(lines, index, mode) {
3077
+ const chunks = [];
3078
+ let at = index;
3079
+ while (at < lines.length && !lines[at].startsWith("***")) {
3080
+ if (!lines[at].startsWith("@@")) {
3081
+ if (mode === "strict") {
3082
+ unexpectedPatchLine("in update body", lines[at]);
3083
+ }
3084
+ at += 1;
3085
+ continue;
3086
+ }
3087
+ const context = lines[at].slice(2).trim() || undefined;
3088
+ at += 1;
3089
+ const old_lines = [];
3090
+ const new_lines = [];
3091
+ let eof = false;
3092
+ while (at < lines.length && !lines[at].startsWith("@@") && (!lines[at].startsWith("***") || lines[at] === "*** End of File")) {
3093
+ const line = lines[at];
3094
+ if (line === "*** End of File") {
3095
+ eof = true;
3096
+ at += 1;
3097
+ break;
3098
+ }
3099
+ if (line.startsWith(" ")) {
3100
+ old_lines.push(line.slice(1));
3101
+ new_lines.push(line.slice(1));
3102
+ at += 1;
3103
+ continue;
3104
+ }
3105
+ if (line.startsWith("-")) {
3106
+ old_lines.push(line.slice(1));
3107
+ at += 1;
3108
+ continue;
3109
+ }
3110
+ if (line.startsWith("+")) {
3111
+ new_lines.push(line.slice(1));
3112
+ at += 1;
3113
+ continue;
3114
+ }
3115
+ if (mode === "strict") {
3116
+ unexpectedPatchLine("in patch chunk", line);
3117
+ }
3118
+ at += 1;
3119
+ }
3120
+ chunks.push({
3121
+ old_lines,
3122
+ new_lines,
3123
+ change_context: context,
3124
+ is_end_of_file: eof || undefined
3125
+ });
3126
+ }
3127
+ return { chunks, next: at };
3128
+ }
3129
+ function parseAdd(lines, index, mode) {
3130
+ let contents = "";
3131
+ let at = index;
3132
+ while (at < lines.length && !lines[at].startsWith("***")) {
3133
+ if (lines[at].startsWith("+")) {
3134
+ contents += `${lines[at].slice(1)}
3135
+ `;
3136
+ at += 1;
3137
+ continue;
3138
+ }
3139
+ if (mode === "strict") {
3140
+ unexpectedPatchLine("in Add File body", lines[at]);
3141
+ }
3142
+ at += 1;
3143
+ }
3144
+ if (contents.endsWith(`
3145
+ `)) {
3146
+ contents = contents.slice(0, -1);
3147
+ }
3148
+ return { content: contents, next: at };
3149
+ }
3150
+ function parsePatchInternal(patchText, mode) {
3151
+ const clean = normalizePatchText(patchText);
3152
+ const lines = clean.split(`
3153
+ `);
3154
+ const begin = lines.findIndex((line) => line.trim() === "*** Begin Patch");
3155
+ const end = lines.findIndex((line) => line.trim() === "*** End Patch");
3156
+ if (begin === -1 || end === -1 || begin >= end) {
3157
+ throw new Error("Invalid patch format: missing Begin/End markers");
3158
+ }
3159
+ if (mode === "strict") {
3160
+ for (const line of lines.slice(0, begin)) {
3161
+ unexpectedPatchLine("before Begin Patch", line);
3162
+ }
3163
+ for (const line of lines.slice(end + 1)) {
3164
+ unexpectedPatchLine("after End Patch", line);
3165
+ }
3166
+ }
3167
+ const hunks = [];
3168
+ let index = begin + 1;
3169
+ while (index < end) {
3170
+ const header = parseHeader(lines, index);
3171
+ if (!header) {
3172
+ if (mode === "strict") {
3173
+ unexpectedPatchLine("between hunks", lines[index]);
3174
+ }
3175
+ index += 1;
3176
+ continue;
3177
+ }
3178
+ if (lines[index].startsWith("*** Add File:")) {
3179
+ const next2 = parseAdd(lines, header.next, mode);
3180
+ hunks.push({
3181
+ type: "add",
3182
+ path: header.file,
3183
+ contents: next2.content
3184
+ });
3185
+ index = next2.next;
3186
+ continue;
3187
+ }
3188
+ if (lines[index].startsWith("*** Delete File:")) {
3189
+ hunks.push({ type: "delete", path: header.file });
3190
+ index = header.next;
3191
+ continue;
3192
+ }
3193
+ const next = parseChunks(lines, header.next, mode);
3194
+ if (mode === "strict" && next.chunks.length === 0) {
3195
+ throw new Error(`Invalid patch format: Update File is missing @@ chunk body: ${header.file}`);
3196
+ }
3197
+ hunks.push({
3198
+ type: "update",
3199
+ path: header.file,
3200
+ move_path: header.move,
3201
+ chunks: next.chunks
3202
+ });
3203
+ index = next.next;
3204
+ }
3205
+ return { hunks };
3206
+ }
3207
+ function parsePatchStrict(patchText) {
3208
+ return parsePatchInternal(patchText, "strict");
3209
+ }
3210
+ function diffMatrix(old_lines, new_lines) {
3211
+ const dp = Array.from({ length: old_lines.length + 1 }, () => Array(new_lines.length + 1).fill(0));
3212
+ for (let oldIndex = 1;oldIndex <= old_lines.length; oldIndex += 1) {
3213
+ for (let newIndex = 1;newIndex <= new_lines.length; newIndex += 1) {
3214
+ dp[oldIndex][newIndex] = old_lines[oldIndex - 1] === new_lines[newIndex - 1] ? dp[oldIndex - 1][newIndex - 1] + 1 : Math.max(dp[oldIndex - 1][newIndex], dp[oldIndex][newIndex - 1]);
3215
+ }
3216
+ }
3217
+ return dp;
3218
+ }
3219
+ function renderChunk(chunk) {
3220
+ const lines = [chunk.change_context ? `@@ ${chunk.change_context}` : "@@"];
3221
+ const dp = diffMatrix(chunk.old_lines, chunk.new_lines);
3222
+ const body = [];
3223
+ let oldIndex = chunk.old_lines.length;
3224
+ let newIndex = chunk.new_lines.length;
3225
+ while (oldIndex > 0 && newIndex > 0) {
3226
+ if (chunk.old_lines[oldIndex - 1] === chunk.new_lines[newIndex - 1]) {
3227
+ body.push(` ${chunk.old_lines[oldIndex - 1]}`);
3228
+ oldIndex -= 1;
3229
+ newIndex -= 1;
3230
+ continue;
3231
+ }
3232
+ if (dp[oldIndex - 1][newIndex] >= dp[oldIndex][newIndex - 1]) {
3233
+ body.push(`-${chunk.old_lines[oldIndex - 1]}`);
3234
+ oldIndex -= 1;
3235
+ continue;
3236
+ }
3237
+ body.push(`+${chunk.new_lines[newIndex - 1]}`);
3238
+ newIndex -= 1;
3239
+ }
3240
+ while (oldIndex > 0) {
3241
+ body.push(`-${chunk.old_lines[oldIndex - 1]}`);
3242
+ oldIndex -= 1;
3243
+ }
3244
+ while (newIndex > 0) {
3245
+ body.push(`+${chunk.new_lines[newIndex - 1]}`);
3246
+ newIndex -= 1;
3247
+ }
3248
+ lines.push(...body.reverse());
3249
+ if (chunk.is_end_of_file) {
3250
+ lines.push("*** End of File");
3251
+ }
3252
+ return lines;
3253
+ }
3254
+ function renderAddContents(contents) {
3255
+ if (contents.length === 0) {
3256
+ return [];
3257
+ }
3258
+ return contents.split(`
3259
+ `).map((line) => `+${line}`);
3260
+ }
3261
+ function formatPatch(patch) {
3262
+ const lines = ["*** Begin Patch"];
3263
+ for (const hunk of patch.hunks) {
3264
+ if (hunk.type === "add") {
3265
+ lines.push(`*** Add File: ${hunk.path}`);
3266
+ lines.push(...renderAddContents(hunk.contents));
3267
+ continue;
3268
+ }
3269
+ if (hunk.type === "delete") {
3270
+ lines.push(`*** Delete File: ${hunk.path}`);
3271
+ continue;
3272
+ }
3273
+ lines.push(`*** Update File: ${hunk.path}`);
3274
+ if (hunk.move_path) {
3275
+ lines.push(`*** Move to: ${hunk.move_path}`);
3276
+ }
3277
+ for (const chunk of hunk.chunks) {
3278
+ lines.push(...renderChunk(chunk));
3279
+ }
3280
+ }
3281
+ lines.push("*** End Patch");
3282
+ return lines.join(`
3283
+ `);
3284
+ }
3285
+
3286
+ // src/hooks/apply-patch/matching.ts
3287
+ var AUTO_RESCUE_COMPARATOR_NAMES = new Set([
3288
+ "exact",
3289
+ "unicode",
3290
+ "trim-end",
3291
+ "unicode-trim-end"
3292
+ ]);
3293
+ function equalExact(a, b) {
3294
+ return a === b;
3295
+ }
3296
+ function equalUnicodeExact(a, b) {
3297
+ return normalizeUnicode(a) === normalizeUnicode(b);
3298
+ }
3299
+ function equalTrimEnd(a, b) {
3300
+ return a.trimEnd() === b.trimEnd();
3301
+ }
3302
+ function equalUnicodeTrimEnd(a, b) {
3303
+ return normalizeUnicode(a.trimEnd()) === normalizeUnicode(b.trimEnd());
3304
+ }
3305
+ function equalTrim(a, b) {
3306
+ return a.trim() === b.trim();
3307
+ }
3308
+ function equalUnicodeTrim(a, b) {
3309
+ return normalizeUnicode(a.trim()) === normalizeUnicode(b.trim());
3310
+ }
3311
+ var comparatorEntries = [
3312
+ { name: "exact", exact: true, same: equalExact },
3313
+ { name: "unicode", exact: false, same: equalUnicodeExact },
3314
+ { name: "trim-end", exact: false, same: equalTrimEnd },
3315
+ {
3316
+ name: "unicode-trim-end",
3317
+ exact: false,
3318
+ same: equalUnicodeTrimEnd
3319
+ },
3320
+ { name: "trim", exact: false, same: equalTrim },
3321
+ { name: "unicode-trim", exact: false, same: equalUnicodeTrim }
3322
+ ];
3323
+ var autoRescueComparatorEntries = comparatorEntries.filter((entry) => AUTO_RESCUE_COMPARATOR_NAMES.has(entry.name));
3324
+ var MAX_LCS_CHUNK_LINES = 48;
3325
+ var MAX_LCS_CANDIDATES = 64;
3326
+ var autoRescueComparators = autoRescueComparatorEntries.map((entry) => entry.same);
3327
+ var permissiveComparators = comparatorEntries.map((entry) => entry.same);
3328
+ function tryMatch(lines, pattern, start, comparator, eof) {
3329
+ if (eof) {
3330
+ const at = lines.length - pattern.length;
3331
+ if (at >= start) {
3332
+ let ok = true;
3333
+ for (let index = 0;index < pattern.length; index += 1) {
3334
+ if (!comparator.same(lines[at + index], pattern[index])) {
3335
+ ok = false;
3336
+ break;
3337
+ }
3338
+ }
3339
+ if (ok) {
3340
+ return {
3341
+ index: at,
3342
+ comparator: comparator.name,
3343
+ exact: comparator.exact
3344
+ };
3345
+ }
3346
+ }
3347
+ }
3348
+ for (let index = start;index <= lines.length - pattern.length; index += 1) {
3349
+ let ok = true;
3350
+ for (let inner = 0;inner < pattern.length; inner += 1) {
3351
+ if (!comparator.same(lines[index + inner], pattern[inner])) {
3352
+ ok = false;
3353
+ break;
3354
+ }
3355
+ }
3356
+ if (ok) {
3357
+ return {
3358
+ index,
3359
+ comparator: comparator.name,
3360
+ exact: comparator.exact
3361
+ };
3362
+ }
3363
+ }
3364
+ return;
3365
+ }
3366
+ function seekMatch(lines, pattern, start, eof = false) {
3367
+ if (pattern.length === 0) {
3368
+ return;
3369
+ }
3370
+ for (const comparator of autoRescueComparatorEntries) {
3371
+ const hit = tryMatch(lines, pattern, start, comparator, eof);
3372
+ if (hit) {
3373
+ return hit;
3374
+ }
3375
+ }
3376
+ return;
3377
+ }
3378
+ function seek(lines, pattern, start, eof = false) {
3379
+ return seekMatch(lines, pattern, start, eof)?.index ?? -1;
3380
+ }
3381
+ function list(lines, pattern, start, same) {
3382
+ if (pattern.length === 0) {
3383
+ return [];
3384
+ }
3385
+ const out = [];
3386
+ for (let index = start;index <= lines.length - pattern.length; index += 1) {
3387
+ let ok = true;
3388
+ for (let inner = 0;inner < pattern.length; inner += 1) {
3389
+ if (!same(lines[index + inner], pattern[inner])) {
3390
+ ok = false;
3391
+ break;
3392
+ }
3393
+ }
3394
+ if (ok) {
3395
+ out.push(index);
3396
+ }
3397
+ }
3398
+ return out;
3399
+ }
3400
+ function sameRescueLine(a, b) {
3401
+ return equalExact(a, b) || equalUnicodeExact(a, b);
3402
+ }
3403
+ function prefix(old_lines, new_lines) {
3404
+ let index = 0;
3405
+ while (index < old_lines.length && index < new_lines.length && sameRescueLine(old_lines[index], new_lines[index])) {
3406
+ index += 1;
3407
+ }
3408
+ return index;
3409
+ }
3410
+ function suffix(old_lines, new_lines, prefixLength) {
3411
+ let index = 0;
3412
+ while (old_lines.length - index - 1 >= prefixLength && new_lines.length - index - 1 >= prefixLength && sameRescueLine(old_lines[old_lines.length - index - 1], new_lines[new_lines.length - index - 1])) {
3413
+ index += 1;
3414
+ }
3415
+ return index;
3416
+ }
3417
+ function rescueByPrefixSuffix(lines, old_lines, new_lines, start) {
3418
+ const prefixLength = prefix(old_lines, new_lines);
3419
+ const suffixLength = suffix(old_lines, new_lines, prefixLength);
3420
+ if (prefixLength === 0 || suffixLength === 0) {
3421
+ return { kind: "miss" };
3422
+ }
3423
+ const left = old_lines.slice(0, prefixLength);
3424
+ const right = old_lines.slice(old_lines.length - suffixLength);
3425
+ const middle = new_lines.slice(prefixLength, new_lines.length - suffixLength);
3426
+ const hits = new Map;
3427
+ for (const same of autoRescueComparators) {
3428
+ for (const leftIndex of list(lines, left, start, same)) {
3429
+ const from = leftIndex + left.length;
3430
+ for (const rightIndex of list(lines, right, from, same)) {
3431
+ const key = `${from}:${rightIndex}`;
3432
+ hits.set(key, {
3433
+ start: from,
3434
+ del: rightIndex - from,
3435
+ add: [...middle]
3436
+ });
3437
+ }
3438
+ }
3439
+ }
3440
+ if (hits.size === 0) {
3441
+ return { kind: "miss" };
3442
+ }
3443
+ if (hits.size > 1) {
3444
+ return { kind: "ambiguous", phase: "prefix_suffix" };
3445
+ }
3446
+ return { kind: "match", hit: [...hits.values()][0] };
3447
+ }
3448
+ function score(a, b) {
3449
+ const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
3450
+ for (let i = 1;i <= a.length; i += 1) {
3451
+ for (let j = 1;j <= b.length; j += 1) {
3452
+ dp[i][j] = normalizeUnicode(a[i - 1].trim()) === normalizeUnicode(b[j - 1].trim()) ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);
3453
+ }
3454
+ }
3455
+ return dp[a.length][b.length];
3456
+ }
3457
+ function normalizeLcsLine(line) {
3458
+ return normalizeUnicode(line).trim();
3459
+ }
3460
+ function countLcsUpperBound(a, b) {
3461
+ const counts = new Map;
3462
+ for (const line of a) {
3463
+ const key = normalizeLcsLine(line);
3464
+ counts.set(key, (counts.get(key) ?? 0) + 1);
3465
+ }
3466
+ let shared = 0;
3467
+ for (const line of b) {
3468
+ const key = normalizeLcsLine(line);
3469
+ const available = counts.get(key) ?? 0;
3470
+ if (available === 0) {
3471
+ continue;
3472
+ }
3473
+ shared += 1;
3474
+ if (available === 1) {
3475
+ counts.delete(key);
3476
+ continue;
3477
+ }
3478
+ counts.set(key, available - 1);
3479
+ }
3480
+ return shared;
3481
+ }
3482
+ function hasStableBorders(oldLines, candidate) {
3483
+ if (oldLines.length === 0 || candidate.length !== oldLines.length) {
3484
+ return false;
3485
+ }
3486
+ const same = autoRescueComparators.some((compare) => compare(oldLines[0], candidate[0]));
3487
+ if (!same) {
3488
+ return false;
3489
+ }
3490
+ if (oldLines.length === 1) {
3491
+ return true;
3492
+ }
3493
+ return autoRescueComparators.some((compare) => compare(oldLines[oldLines.length - 1], candidate[candidate.length - 1]));
3494
+ }
3495
+ function collectBorderAnchoredStarts(lines, oldLines, start) {
3496
+ if (oldLines.length === 0) {
3497
+ return [];
3498
+ }
3499
+ const firstHits = new Set;
3500
+ const lastHits = new Set;
3501
+ const lastLine = oldLines[oldLines.length - 1];
3502
+ for (const same of autoRescueComparators) {
3503
+ for (const index of list(lines, [oldLines[0]], start, same)) {
3504
+ firstHits.add(index);
3505
+ }
3506
+ for (const index of list(lines, [lastLine], start, same)) {
3507
+ lastHits.add(index);
3508
+ }
3509
+ }
3510
+ const candidates = [];
3511
+ for (const index of [...firstHits].sort((a, b) => a - b)) {
3512
+ const end = index + oldLines.length - 1;
3513
+ if (end >= lines.length || !lastHits.has(end)) {
3514
+ continue;
3515
+ }
3516
+ const candidate = lines.slice(index, index + oldLines.length);
3517
+ if (!hasStableBorders(oldLines, candidate)) {
3518
+ continue;
3519
+ }
3520
+ candidates.push(index);
3521
+ }
3522
+ return candidates;
3523
+ }
3524
+ function rescueByLcs(lines, old_lines, new_lines, start) {
3525
+ if (old_lines.length === 0 || lines.length === 0) {
3526
+ return { kind: "miss" };
3527
+ }
3528
+ const from = start;
3529
+ const to = lines.length - old_lines.length;
3530
+ if (to < from) {
3531
+ return { kind: "miss" };
3532
+ }
3533
+ if (old_lines.length > MAX_LCS_CHUNK_LINES) {
3534
+ return { kind: "miss" };
3535
+ }
3536
+ const needed = old_lines.length <= 2 ? old_lines.length : Math.max(2, Math.ceil(old_lines.length * 0.7));
3537
+ const candidates = collectBorderAnchoredStarts(lines, old_lines, start);
3538
+ if (candidates.length === 0 || candidates.length > MAX_LCS_CANDIDATES) {
3539
+ return { kind: "miss" };
3540
+ }
3541
+ let best;
3542
+ let bestScore = 0;
3543
+ let ties = 0;
3544
+ for (const index of candidates) {
3545
+ if (index < from || index > to) {
3546
+ continue;
3547
+ }
3548
+ const window = lines.slice(index, index + old_lines.length);
3549
+ if (countLcsUpperBound(old_lines, window) < needed) {
3550
+ continue;
3551
+ }
3552
+ const current = score(old_lines, window);
3553
+ if (current > bestScore) {
3554
+ bestScore = current;
3555
+ ties = 1;
3556
+ best = {
3557
+ start: index,
3558
+ del: old_lines.length,
3559
+ add: [...new_lines]
3560
+ };
3561
+ continue;
3562
+ }
3563
+ if (current === bestScore && current > 0) {
3564
+ ties += 1;
3565
+ }
3566
+ }
3567
+ if (!best || bestScore < needed) {
3568
+ return { kind: "miss" };
3569
+ }
3570
+ if (ties > 1) {
3571
+ return { kind: "ambiguous", phase: "lcs" };
3572
+ }
3573
+ return { kind: "match", hit: best };
3574
+ }
3575
+
3576
+ // src/hooks/apply-patch/resolution.ts
3577
+ function splitFileLines(text) {
3578
+ const eol = text.match(/\r\n|\n|\r/)?.[0] === `\r
3579
+ ` ? `\r
3580
+ ` : `
3581
+ `;
3582
+ const normalized = text.replace(/\r\n/g, `
3583
+ `).replace(/\r/g, `
3584
+ `);
3585
+ const hasFinalNewline = normalized.endsWith(`
3586
+ `);
3587
+ const lines = normalized.split(`
3588
+ `);
3589
+ if (hasFinalNewline) {
3590
+ lines.pop();
3591
+ }
3592
+ return { lines, eol, hasFinalNewline };
3593
+ }
3594
+ function resolveChunkStart(lines, chunk, start) {
3595
+ if (!chunk.change_context) {
3596
+ return start;
3597
+ }
3598
+ const at = seek(lines, [chunk.change_context], start);
3599
+ return at === -1 ? start : at + 1;
3600
+ }
3601
+ function resolveUniqueAnchor(lines, changeContext, start) {
3602
+ const hits = new Set;
3603
+ for (const same of autoRescueComparators) {
3604
+ for (const index2 of list(lines, [changeContext], start, same)) {
3605
+ hits.add(index2);
3606
+ }
3607
+ }
3608
+ if (hits.size === 0) {
3609
+ return { kind: "missing" };
3610
+ }
3611
+ if (hits.size > 1) {
3612
+ return { kind: "ambiguous" };
3613
+ }
3614
+ const index = [...hits][0];
3615
+ const canonicalLine = lines[index];
3616
+ const comparator = seekMatch(lines, [changeContext], index)?.comparator;
3617
+ return {
3618
+ kind: "match",
3619
+ index,
3620
+ exact: canonicalLine === changeContext,
3621
+ comparator: comparator ?? "exact",
3622
+ canonicalLine
3623
+ };
3624
+ }
3625
+ function locateChunk(lines, file, chunk, start, cfg) {
3626
+ const old_lines = chunk.old_lines;
3627
+ const new_lines = chunk.new_lines;
3628
+ const match = seekMatch(lines, old_lines, start, chunk.is_end_of_file ?? false);
3629
+ if (match) {
3630
+ const canonical_old_lines = lines.slice(match.index, match.index + old_lines.length);
3631
+ const rewritten = !match.exact;
3632
+ return {
3633
+ hit: { start: match.index, del: old_lines.length, add: [...new_lines] },
3634
+ old_lines,
3635
+ canonical_old_lines,
3636
+ canonical_new_lines: [...chunk.new_lines],
3637
+ resolved_is_end_of_file: match.index + canonical_old_lines.length === lines.length,
3638
+ rewritten,
3639
+ strategy: undefined,
3640
+ matchComparator: match.comparator
3641
+ };
3642
+ }
3643
+ if (cfg.prefixSuffix) {
3644
+ const rescued = rescueByPrefixSuffix(lines, old_lines, new_lines, start);
3645
+ if (rescued.kind === "ambiguous") {
3646
+ throw new Error(`Prefix/suffix rescue was ambiguous in ${file}:
3647
+ ${chunk.old_lines.join(`
3648
+ `)}`);
3649
+ }
3650
+ if (rescued.kind === "match") {
3651
+ const prefixLength = prefix(old_lines, new_lines);
3652
+ const suffixLength = suffix(old_lines, new_lines, prefixLength);
3653
+ const canonicalStart = rescued.hit.start - prefixLength;
3654
+ const canonicalEnd = rescued.hit.start + rescued.hit.del + suffixLength;
3655
+ return {
3656
+ hit: rescued.hit,
3657
+ old_lines,
3658
+ canonical_old_lines: lines.slice(canonicalStart, canonicalEnd),
3659
+ canonical_new_lines: [...chunk.new_lines],
3660
+ resolved_is_end_of_file: canonicalEnd === lines.length,
3661
+ rewritten: true,
3662
+ strategy: "prefix/suffix",
3663
+ matchComparator: "exact"
3664
+ };
3665
+ }
3666
+ }
3667
+ if (cfg.lcsRescue) {
3668
+ const rescued = rescueByLcs(lines, old_lines, new_lines, start);
3669
+ if (rescued.kind === "ambiguous") {
3670
+ throw new Error(`LCS rescue was ambiguous in ${file}:
3671
+ ${chunk.old_lines.join(`
3672
+ `)}`);
3673
+ }
3674
+ if (rescued.kind === "match") {
3675
+ return {
3676
+ hit: rescued.hit,
3677
+ old_lines,
3678
+ canonical_old_lines: lines.slice(rescued.hit.start, rescued.hit.start + rescued.hit.del),
3679
+ canonical_new_lines: [...chunk.new_lines],
3680
+ resolved_is_end_of_file: rescued.hit.start + rescued.hit.del === lines.length,
3681
+ rewritten: true,
3682
+ strategy: "lcs",
3683
+ matchComparator: "exact"
3684
+ };
3685
+ }
3686
+ }
3687
+ throw new Error(`Failed to find expected lines in ${file}:
3688
+ ${chunk.old_lines.join(`
3689
+ `)}`);
3690
+ }
3691
+ function applyHits(lines, hits, eol = `
3692
+ `, hasFinalNewline = true) {
3693
+ const out = [...lines];
3694
+ for (let index = hits.length - 1;index >= 0; index -= 1) {
3695
+ out.splice(hits[index].start, hits[index].del, ...hits[index].add);
3696
+ }
3697
+ if (out.length === 0) {
3698
+ return "";
3699
+ }
3700
+ const rendered = out.join(eol);
3701
+ return hasFinalNewline ? `${rendered}${eol}` : rendered;
3702
+ }
3703
+ function resolveUpdateChunksFromFileLines(file, state, chunks, cfg) {
3704
+ const lines = [...state.lines];
3705
+ const resolved = [];
3706
+ let start = 0;
3707
+ for (const chunk of chunks) {
3708
+ const chunkStart = resolveChunkStart(lines, chunk, start);
3709
+ let strategy;
3710
+ if (chunk.old_lines.length === 0) {
3711
+ if (chunk.is_end_of_file) {
3712
+ resolved.push({
3713
+ hit: {
3714
+ start: lines.length,
3715
+ del: 0,
3716
+ add: [...chunk.new_lines]
3717
+ },
3718
+ old_lines: [],
3719
+ canonical_old_lines: [],
3720
+ canonical_new_lines: [...chunk.new_lines],
3721
+ resolved_is_end_of_file: true,
3722
+ rewritten: false,
3723
+ strategy,
3724
+ matchComparator: "exact"
3725
+ });
3726
+ start = lines.length;
3727
+ continue;
3728
+ }
3729
+ if (!chunk.change_context) {
3730
+ throw new Error(`Missing insertion anchor in ${file}`);
3731
+ }
3732
+ const anchorMatch = resolveUniqueAnchor(lines, chunk.change_context, start);
3733
+ if (anchorMatch.kind === "missing") {
3734
+ throw new Error(`Failed to find insertion anchor in ${file}:
3735
+ ${chunk.change_context}`);
3736
+ }
3737
+ if (anchorMatch.kind === "ambiguous") {
3738
+ throw new Error(`Insertion anchor was ambiguous in ${file}:
3739
+ ${chunk.change_context}`);
3740
+ }
3741
+ const insertAt = anchorMatch.index + 1;
3742
+ if (insertAt === lines.length) {
3743
+ resolved.push({
3744
+ hit: {
3745
+ start: insertAt,
3746
+ del: 0,
3747
+ add: [...chunk.new_lines]
3748
+ },
3749
+ old_lines: [],
3750
+ canonical_old_lines: [],
3751
+ canonical_new_lines: [...chunk.new_lines],
3752
+ canonical_change_context: anchorMatch.exact ? undefined : anchorMatch.canonicalLine,
3753
+ resolved_is_end_of_file: insertAt === lines.length,
3754
+ rewritten: !anchorMatch.exact,
3755
+ strategy: anchorMatch.exact ? strategy : "anchor",
3756
+ matchComparator: anchorMatch.comparator
3757
+ });
3758
+ start = insertAt;
3759
+ continue;
3760
+ }
3761
+ const anchor = lines[insertAt];
3762
+ strategy = "anchor";
3763
+ resolved.push({
3764
+ hit: {
3765
+ start: insertAt,
3766
+ del: 0,
3767
+ add: [...chunk.new_lines]
3768
+ },
3769
+ old_lines: [],
3770
+ canonical_old_lines: [anchor],
3771
+ canonical_new_lines: [...chunk.new_lines, anchor],
3772
+ resolved_is_end_of_file: insertAt + 1 === lines.length,
3773
+ rewritten: true,
3774
+ strategy,
3775
+ matchComparator: "exact"
3776
+ });
3777
+ start = insertAt;
3778
+ continue;
3779
+ }
3780
+ const found = locateChunk(lines, file, chunk, chunkStart, cfg);
3781
+ resolved.push(found);
3782
+ start = found.hit.start + found.hit.del;
3783
+ }
3784
+ resolved.sort((a, b) => a.hit.start - b.hit.start);
3785
+ for (let index = 1;index < resolved.length; index += 1) {
3786
+ const previous = resolved[index - 1].hit;
3787
+ const current = resolved[index].hit;
3788
+ if (previous.start + previous.del > current.start) {
3789
+ throw new Error(`Overlapping patch chunks in ${file}`);
3790
+ }
3791
+ }
3792
+ return {
3793
+ lines,
3794
+ resolved,
3795
+ eol: state.eol,
3796
+ hasFinalNewline: state.hasFinalNewline
3797
+ };
3798
+ }
3799
+ function deriveNewContentFromText(file, text, chunks, cfg) {
3800
+ const { lines, resolved, eol, hasFinalNewline } = resolveUpdateChunksFromFileLines(file, splitFileLines(text), chunks, cfg);
3801
+ return applyHits(lines, resolved.map((chunk) => chunk.hit), eol, hasFinalNewline);
3802
+ }
3803
+ function resolveUpdateChunksFromText(file, text, chunks, cfg) {
3804
+ return resolveUpdateChunksFromFileLines(file, splitFileLines(text), chunks, cfg);
3805
+ }
3806
+
3807
+ // src/hooks/apply-patch/execution-context.ts
3808
+ function isMissingPathError(error) {
3809
+ return !!error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
3810
+ }
3811
+ async function real(target) {
3812
+ const parts = [];
3813
+ let current = path3.resolve(target);
3814
+ while (true) {
3815
+ const exact = await fs3.realpath(current).catch((error) => {
3816
+ if (isMissingPathError(error)) {
3817
+ return null;
3818
+ }
3819
+ throw createApplyPatchInternalError(`Failed to resolve real path: ${current}`, error);
3820
+ });
3821
+ if (exact) {
3822
+ return parts.length === 0 ? exact : path3.join(exact, ...parts.reverse());
3823
+ }
3824
+ const parent = path3.dirname(current);
3825
+ if (parent === current) {
3826
+ return parts.length === 0 ? current : path3.join(current, ...parts.reverse());
3827
+ }
3828
+ parts.push(path3.basename(current));
3829
+ current = parent;
3830
+ }
3831
+ }
3832
+ function inside(root, target) {
3833
+ const rel = path3.relative(root, target);
3834
+ return rel === "" || !rel.startsWith("..") && !path3.isAbsolute(rel);
3835
+ }
3836
+ function createPathGuardContext(root, worktree) {
3837
+ return {
3838
+ rootReal: real(root),
3839
+ worktreeReal: worktree && worktree !== "/" ? real(worktree) : undefined,
3840
+ realCache: new Map
3841
+ };
3842
+ }
3843
+ async function realCached(ctx, target) {
3844
+ const resolvedTarget = path3.resolve(target);
3845
+ let pending = ctx.realCache.get(resolvedTarget);
3846
+ if (!pending) {
3847
+ pending = real(resolvedTarget);
3848
+ ctx.realCache.set(resolvedTarget, pending);
3849
+ }
3850
+ return await pending;
3851
+ }
3852
+ async function guard(ctx, target) {
3853
+ const [targetReal, rootReal] = await Promise.all([
3854
+ realCached(ctx, target),
3855
+ ctx.rootReal
3856
+ ]);
3857
+ if (inside(rootReal, targetReal)) {
3858
+ return;
3859
+ }
3860
+ if (!ctx.worktreeReal) {
3861
+ throw createApplyPatchBlockedError(`patch contains path outside workspace root: ${target}`);
3862
+ }
3863
+ const treeReal = await ctx.worktreeReal;
3864
+ if (inside(treeReal, targetReal)) {
3865
+ return;
3866
+ }
3867
+ throw createApplyPatchBlockedError(`patch contains path outside workspace root: ${target}`);
3868
+ }
3869
+ function createFileCacheContext() {
3870
+ return { stats: new Map };
3871
+ }
3872
+ async function statCached(ctx, filePath) {
3873
+ let pending = ctx.stats.get(filePath);
3874
+ if (!pending) {
3875
+ const nextPending = fs3.stat(filePath).catch((error) => {
3876
+ if (isMissingPathError(error)) {
3877
+ return null;
3878
+ }
3879
+ throw createApplyPatchInternalError(`Failed to stat file for patch verification: ${filePath}`, error);
3880
+ });
3881
+ ctx.stats.set(filePath, nextPending);
3882
+ pending = nextPending;
3883
+ }
3884
+ return await pending;
3885
+ }
3886
+ async function assertRegularFile(ctx, filePath, verb) {
3887
+ const stat2 = await statCached(ctx, filePath);
3888
+ if (!stat2 || stat2.isDirectory()) {
3889
+ throw createApplyPatchVerificationError(`Failed to read file to ${verb}: ${filePath}`);
3890
+ }
3891
+ }
3892
+ function collectPatchTargets(root, hunks) {
3893
+ const targets = new Set;
3894
+ for (const hunk of hunks) {
3895
+ targets.add(path3.resolve(root, hunk.path));
3896
+ if (hunk.type === "update" && hunk.move_path) {
3897
+ targets.add(path3.resolve(root, hunk.move_path));
3898
+ }
3899
+ }
3900
+ return [...targets];
3901
+ }
3902
+ function validatePatchPaths(hunks) {
3903
+ for (const hunk of hunks) {
3904
+ if (path3.isAbsolute(hunk.path)) {
3905
+ throw createApplyPatchValidationError(`absolute patch paths are not allowed: ${hunk.path}`);
3906
+ }
3907
+ if (hunk.type === "update" && hunk.move_path && path3.isAbsolute(hunk.move_path)) {
3908
+ throw createApplyPatchValidationError(`absolute patch paths are not allowed: ${hunk.move_path}`);
3909
+ }
3910
+ }
3911
+ }
3912
+ async function guardPatchTargets(root, worktree, targets) {
3913
+ const guardContext = createPathGuardContext(root, worktree);
3914
+ for (const target of targets) {
3915
+ await guard(guardContext, target);
3916
+ }
3917
+ return targets.length;
3918
+ }
3919
+ function parseValidatedPatch(patchText) {
3920
+ let hunks;
3921
+ try {
3922
+ hunks = parsePatchStrict(patchText).hunks;
3923
+ } catch (error) {
3924
+ throw createApplyPatchValidationError(getErrorMessage(error));
3925
+ }
3926
+ if (hunks.length === 0) {
3927
+ const clean = patchText.replace(/\r\n/g, `
3928
+ `).replace(/\r/g, `
3929
+ `).trim();
3930
+ if (clean === `*** Begin Patch
3931
+ *** End Patch`) {
3932
+ throw createApplyPatchValidationError("empty patch");
3933
+ }
3934
+ throw createApplyPatchValidationError("no hunks found");
3935
+ }
3936
+ validatePatchPaths(hunks);
3937
+ return hunks;
3938
+ }
3939
+ async function readPreparedFileText(filePath, verb) {
3940
+ try {
3941
+ return await fs3.readFile(filePath, "utf-8");
3942
+ } catch (error) {
3943
+ if (isMissingPathError(error)) {
3944
+ throw createApplyPatchVerificationError(`Failed to read file to ${verb}: ${filePath}`);
3945
+ }
3946
+ throw createApplyPatchInternalError(`Failed to read file for patch verification: ${filePath}`, error);
3947
+ }
3948
+ }
3949
+ async function createPatchExecutionContext(root, patchText, worktree) {
3950
+ const hunks = parseValidatedPatch(patchText);
3951
+ await guardPatchTargets(root, worktree, collectPatchTargets(root, hunks));
3952
+ const files = createFileCacheContext();
3953
+ const staged = new Map;
3954
+ async function assertPreparedPathMissing(filePath, verb) {
3955
+ const existing = staged.get(filePath);
3956
+ if (existing) {
3957
+ if (!existing.exists) {
3958
+ return;
3959
+ }
3960
+ throw createApplyPatchVerificationError(verb === "add" ? `Add File target already exists: ${filePath}` : `Move destination already exists: ${filePath}`);
3961
+ }
3962
+ const stat2 = await statCached(files, filePath);
3963
+ if (!stat2) {
3964
+ return;
3965
+ }
3966
+ throw createApplyPatchVerificationError(verb === "add" ? `Add File target already exists: ${filePath}` : `Move destination already exists: ${filePath}`);
3967
+ }
3968
+ async function getPreparedFileState(filePath, verb) {
3969
+ const existing = staged.get(filePath);
3970
+ if (existing) {
3971
+ if (!existing.exists) {
3972
+ throw createApplyPatchVerificationError(`Failed to read file to ${verb}: ${filePath}`);
3973
+ }
3974
+ return existing;
3975
+ }
3976
+ await assertRegularFile(files, filePath, verb);
3977
+ const stat2 = await statCached(files, filePath);
3978
+ const text = await readPreparedFileText(filePath, verb);
3979
+ const state = {
3980
+ exists: true,
3981
+ text,
3982
+ mode: stat2 ? stat2.mode & 4095 : undefined,
3983
+ derived: false
3984
+ };
3985
+ staged.set(filePath, state);
3986
+ return state;
3987
+ }
3988
+ return {
3989
+ hunks,
3990
+ staged,
3991
+ getPreparedFileState,
3992
+ assertPreparedPathMissing
3993
+ };
3994
+ }
3995
+ function resolvePreparedUpdate(filePath, currentText, hunk, cfg) {
3996
+ try {
3997
+ const { lines, resolved, eol, hasFinalNewline } = resolveUpdateChunksFromText(filePath, currentText, hunk.chunks, cfg);
3998
+ return {
3999
+ resolved,
4000
+ nextText: applyHits(lines, resolved.map((chunk) => chunk.hit), eol, hasFinalNewline)
4001
+ };
4002
+ } catch (error) {
4003
+ throw createApplyPatchVerificationError(getErrorMessage(error), error);
4004
+ }
4005
+ }
4006
+ function stageAddedText(contents) {
4007
+ return contents.length === 0 || contents.endsWith(`
4008
+ `) ? contents : `${contents}
4009
+ `;
4010
+ }
4011
+ // src/hooks/apply-patch/rewrite.ts
4012
+ import path4 from "path";
4013
+ function normalizeTextLineEndings(text) {
4014
+ return text.replace(/\r\n/g, `
4015
+ `).replace(/\r/g, `
4016
+ `);
4017
+ }
4018
+ function splitPatchTextLines(text) {
4019
+ const normalized = normalizeTextLineEndings(text);
4020
+ const lines = normalized.split(`
4021
+ `);
4022
+ if (normalized.endsWith(`
4023
+ `)) {
4024
+ lines.pop();
4025
+ }
4026
+ return lines;
4027
+ }
4028
+ function createCollapsedUpdateHunk(pathValue, filePath, baseText, finalText, cfg, movePath) {
4029
+ const collapsedChunk = {
4030
+ old_lines: splitPatchTextLines(baseText),
4031
+ new_lines: splitPatchTextLines(finalText),
4032
+ change_context: undefined,
4033
+ is_end_of_file: true
4034
+ };
4035
+ const minimizedChunk = minimizeMergedChunk(collapsedChunk);
4036
+ const chunk = minimizedChunk.old_lines.length === collapsedChunk.old_lines.length && minimizedChunk.new_lines.length === collapsedChunk.new_lines.length && minimizedChunk.change_context === collapsedChunk.change_context && minimizedChunk.is_end_of_file === collapsedChunk.is_end_of_file ? collapsedChunk : (() => {
4037
+ try {
4038
+ return deriveNewContentFromText(filePath, baseText, [minimizedChunk], cfg) === finalText ? minimizedChunk : collapsedChunk;
4039
+ } catch {
4040
+ return collapsedChunk;
4041
+ }
4042
+ })();
4043
+ return {
4044
+ type: "update",
4045
+ path: pathValue,
4046
+ move_path: movePath,
4047
+ chunks: [chunk]
4048
+ };
4049
+ }
4050
+ function clonePatchChunks(chunks) {
4051
+ return chunks.map((chunk) => ({
4052
+ old_lines: [...chunk.old_lines],
4053
+ new_lines: [...chunk.new_lines],
4054
+ change_context: chunk.change_context,
4055
+ is_end_of_file: chunk.is_end_of_file
4056
+ }));
4057
+ }
4058
+ function minimizeMergedChunk(chunk) {
4059
+ if (chunk.old_lines.length === 0 && chunk.new_lines.length === 0) {
4060
+ return {
4061
+ old_lines: [],
4062
+ new_lines: [],
4063
+ change_context: chunk.change_context,
4064
+ is_end_of_file: chunk.is_end_of_file
4065
+ };
4066
+ }
4067
+ let prefixLength = 0;
4068
+ while (prefixLength < chunk.old_lines.length && prefixLength < chunk.new_lines.length && chunk.old_lines[prefixLength] === chunk.new_lines[prefixLength]) {
4069
+ prefixLength += 1;
4070
+ }
4071
+ let suffixLength = 0;
4072
+ while (chunk.old_lines.length - suffixLength - 1 >= prefixLength && chunk.new_lines.length - suffixLength - 1 >= prefixLength && chunk.old_lines[chunk.old_lines.length - suffixLength - 1] === chunk.new_lines[chunk.new_lines.length - suffixLength - 1]) {
4073
+ suffixLength += 1;
4074
+ }
4075
+ if (prefixLength === 0 && suffixLength === 0) {
4076
+ return {
4077
+ old_lines: [...chunk.old_lines],
4078
+ new_lines: [...chunk.new_lines],
4079
+ change_context: chunk.change_context,
4080
+ is_end_of_file: chunk.is_end_of_file
4081
+ };
4082
+ }
4083
+ return {
4084
+ old_lines: chunk.old_lines.slice(prefixLength, chunk.old_lines.length - suffixLength),
4085
+ new_lines: chunk.new_lines.slice(prefixLength, chunk.new_lines.length - suffixLength),
4086
+ change_context: prefixLength > 0 ? chunk.old_lines[prefixLength - 1] : chunk.change_context,
4087
+ is_end_of_file: chunk.is_end_of_file && suffixLength === 0 ? true : undefined
4088
+ };
4089
+ }
4090
+ function createUpdateHunk(pathValue, chunks, movePath) {
4091
+ return {
4092
+ type: "update",
4093
+ path: pathValue,
4094
+ move_path: movePath,
4095
+ chunks: clonePatchChunks(chunks)
4096
+ };
4097
+ }
4098
+ function mergeSameFileUpdateGroupChunks(filePath, group, nextChunks, finalText, cfg) {
4099
+ if (!group.chunks) {
4100
+ return;
4101
+ }
4102
+ const mergedChunks = [
4103
+ ...clonePatchChunks(group.chunks).map(minimizeMergedChunk),
4104
+ ...clonePatchChunks(nextChunks).map(minimizeMergedChunk)
4105
+ ];
4106
+ try {
4107
+ const mergedText = deriveNewContentFromText(filePath, group.baseText, mergedChunks, cfg);
4108
+ return mergedText === finalText ? mergedChunks : undefined;
4109
+ } catch {
4110
+ return;
4111
+ }
4112
+ }
4113
+ function addContentsFromFinalText(text) {
4114
+ return text.endsWith(`
4115
+ `) ? text.slice(0, -1) : text;
4116
+ }
4117
+ function renderRewriteDependencyGroup(group, cfg) {
4118
+ if (group.kind === "add") {
4119
+ return {
4120
+ type: "add",
4121
+ path: group.group.outputPath,
4122
+ contents: addContentsFromFinalText(group.group.finalText)
4123
+ };
4124
+ }
4125
+ return group.group.chunks ? createUpdateHunk(group.group.sourcePath, group.group.chunks, group.group.outputPath !== group.group.sourcePath ? group.group.outputPath : undefined) : createCollapsedUpdateHunk(group.group.sourcePath, group.group.sourceFilePath, group.group.baseText, group.group.finalText, cfg, group.group.outputPath !== group.group.sourcePath ? group.group.outputPath : undefined);
4126
+ }
4127
+ function rewriteModeForDependentUpdate(group) {
4128
+ if (group.kind === "add") {
4129
+ return "collapse:add-followed-by-update";
4130
+ }
4131
+ if (group.group.outputPath !== group.group.sourcePath) {
4132
+ return "collapse:move-followed-by-update";
4133
+ }
4134
+ return "merge:same-file-updates";
4135
+ }
4136
+ function combineDependentUpdateGroup(filePath, group, nextChunks, finalText, nextOutputPath, nextOutputFilePath, cfg) {
4137
+ if (group.kind === "add") {
4138
+ return {
4139
+ kind: "add",
4140
+ group: {
4141
+ ...group.group,
4142
+ outputPath: nextOutputPath,
4143
+ outputFilePath: nextOutputFilePath,
4144
+ finalText
4145
+ }
4146
+ };
4147
+ }
4148
+ const mergedChunks = group.group.outputFilePath === filePath && group.group.sourceFilePath === filePath && nextOutputFilePath === filePath ? mergeSameFileUpdateGroupChunks(filePath, group.group, nextChunks, finalText, cfg) : undefined;
4149
+ return {
4150
+ kind: "update",
4151
+ group: {
4152
+ ...group.group,
4153
+ outputPath: nextOutputPath,
4154
+ outputFilePath: nextOutputFilePath,
4155
+ finalText,
4156
+ chunks: mergedChunks
4157
+ }
4158
+ };
4159
+ }
4160
+ async function rewritePatch(root, patchText, cfg, worktree) {
4161
+ try {
4162
+ let clearDependencyGroup = function(filePath) {
4163
+ dependencyGroups.delete(filePath);
4164
+ };
4165
+ const { hunks, staged, getPreparedFileState, assertPreparedPathMissing } = await createPatchExecutionContext(root, patchText, worktree);
4166
+ const normalizedPatchText = normalizePatchText(patchText);
4167
+ const rewritten = [];
4168
+ let changed = false;
4169
+ let rewrittenChunks = 0;
4170
+ const rewriteModes = new Set;
4171
+ const totalChunks = hunks.reduce((count, hunk) => count + (hunk.type === "update" ? hunk.chunks.length : 0), 0);
4172
+ const dependencyGroups = new Map;
4173
+ for (const hunk of hunks) {
4174
+ if (hunk.type === "add") {
4175
+ const filePath2 = path4.resolve(root, hunk.path);
4176
+ await assertPreparedPathMissing(filePath2, "add");
4177
+ rewritten.push(hunk);
4178
+ clearDependencyGroup(filePath2);
4179
+ const finalText = stageAddedText(hunk.contents);
4180
+ staged.set(filePath2, {
4181
+ exists: true,
4182
+ text: finalText,
4183
+ derived: true
4184
+ });
4185
+ dependencyGroups.set(filePath2, {
4186
+ kind: "add",
4187
+ group: {
4188
+ index: rewritten.length - 1,
4189
+ outputPath: hunk.path,
4190
+ outputFilePath: filePath2,
4191
+ finalText
4192
+ }
4193
+ });
4194
+ continue;
4195
+ }
4196
+ if (hunk.type === "delete") {
4197
+ const filePath2 = path4.resolve(root, hunk.path);
4198
+ await getPreparedFileState(filePath2, "delete");
4199
+ clearDependencyGroup(filePath2);
4200
+ rewritten.push(hunk);
4201
+ staged.set(filePath2, { exists: false, derived: true });
4202
+ continue;
4203
+ }
4204
+ const filePath = path4.resolve(root, hunk.path);
4205
+ const currentDependency = dependencyGroups.get(filePath);
4206
+ const current = await getPreparedFileState(filePath, "update");
4207
+ if (!current.exists) {
4208
+ throw createApplyPatchVerificationError(`Failed to read file to update: ${filePath}`);
4209
+ }
4210
+ const movePath = hunk.move_path ? path4.resolve(root, hunk.move_path) : undefined;
4211
+ if (movePath && movePath !== filePath) {
4212
+ await assertPreparedPathMissing(movePath, "move");
4213
+ }
4214
+ const { resolved, nextText } = resolvePreparedUpdate(filePath, current.text, hunk, cfg);
4215
+ const next = resolved.map((chunk, index) => ({
4216
+ old_lines: [...chunk.canonical_old_lines],
4217
+ new_lines: [...chunk.canonical_new_lines],
4218
+ change_context: chunk.canonical_change_context ?? hunk.chunks[index].change_context,
4219
+ is_end_of_file: hunk.chunks[index].is_end_of_file && chunk.resolved_is_end_of_file ? true : undefined
4220
+ }));
4221
+ for (const chunk of resolved) {
4222
+ if (!chunk.rewritten) {
4223
+ continue;
4224
+ }
4225
+ changed = true;
4226
+ rewrittenChunks += 1;
4227
+ if (chunk.strategy) {
4228
+ rewriteModes.add(chunk.strategy);
4229
+ continue;
4230
+ }
4231
+ if (chunk.matchComparator && chunk.matchComparator !== "exact") {
4232
+ rewriteModes.add(`match:${chunk.matchComparator}`);
4233
+ }
4234
+ }
4235
+ const nextOutputPath = hunk.move_path ?? hunk.path;
4236
+ const nextOutputFilePath = movePath ?? filePath;
4237
+ if (current.derived && currentDependency) {
4238
+ const nextGroup = combineDependentUpdateGroup(filePath, currentDependency, next, nextText, nextOutputPath, nextOutputFilePath, cfg);
4239
+ rewritten[currentDependency.group.index] = renderRewriteDependencyGroup(nextGroup, cfg);
4240
+ changed = true;
4241
+ rewriteModes.add(rewriteModeForDependentUpdate(currentDependency));
4242
+ clearDependencyGroup(filePath);
4243
+ if (movePath && movePath !== filePath) {
4244
+ clearDependencyGroup(movePath);
4245
+ }
4246
+ dependencyGroups.set(nextOutputFilePath, nextGroup);
4247
+ } else {
4248
+ rewritten.push(createUpdateHunk(hunk.path, next, hunk.move_path));
4249
+ clearDependencyGroup(filePath);
4250
+ if (movePath && movePath !== filePath) {
4251
+ clearDependencyGroup(movePath);
4252
+ }
4253
+ dependencyGroups.set(nextOutputFilePath, {
4254
+ kind: "update",
4255
+ group: {
4256
+ index: rewritten.length - 1,
4257
+ sourcePath: hunk.path,
4258
+ outputPath: nextOutputPath,
4259
+ sourceFilePath: filePath,
4260
+ outputFilePath: nextOutputFilePath,
4261
+ baseText: current.text,
4262
+ finalText: nextText,
4263
+ chunks: clonePatchChunks(next)
4264
+ }
4265
+ });
4266
+ }
4267
+ if (movePath && movePath !== filePath) {
4268
+ staged.set(filePath, { exists: false, derived: true });
4269
+ staged.set(movePath, {
4270
+ exists: true,
4271
+ text: nextText,
4272
+ mode: current.mode,
4273
+ derived: true
4274
+ });
4275
+ } else {
4276
+ staged.set(filePath, {
4277
+ exists: true,
4278
+ text: nextText,
4279
+ mode: current.mode,
4280
+ derived: true
4281
+ });
4282
+ }
4283
+ }
4284
+ if (!changed) {
4285
+ if (normalizedPatchText !== patchText) {
4286
+ return {
4287
+ patchText: normalizedPatchText,
4288
+ changed: true,
4289
+ rewrittenChunks: 0,
4290
+ totalChunks,
4291
+ rewriteModes: ["normalize:patch-text"]
4292
+ };
4293
+ }
4294
+ return {
4295
+ patchText,
4296
+ changed: false,
4297
+ rewrittenChunks: 0,
4298
+ totalChunks,
4299
+ rewriteModes: []
4300
+ };
4301
+ }
4302
+ return {
4303
+ patchText: formatPatch({ hunks: rewritten }),
4304
+ changed: true,
4305
+ rewrittenChunks,
4306
+ totalChunks,
4307
+ rewriteModes: [...rewriteModes].sort()
4308
+ };
4309
+ } catch (error) {
4310
+ throw ensureApplyPatchError(error, "Unexpected rewrite failure");
4311
+ }
4312
+ }
4313
+ // src/hooks/apply-patch/index.ts
4314
+ var APPLY_PATCH_RESCUE_OPTIONS = {
4315
+ prefixSuffix: true,
4316
+ lcsRescue: true
4317
+ };
4318
+ function createApplyPatchHook(ctx) {
4319
+ function logHookStatus(state, data) {
4320
+ log(`apply-patch hook ${state}`, data);
4321
+ }
4322
+ return {
4323
+ "tool.execute.before": async (input, output) => {
4324
+ if (input.tool !== "apply_patch") {
4325
+ return;
4326
+ }
4327
+ if (typeof output.args?.patchText !== "string") {
4328
+ return;
4329
+ }
4330
+ const root = input.directory || ctx.directory || process.cwd();
4331
+ const worktree = ctx.worktree || root;
4332
+ try {
4333
+ const result = await rewritePatch(root, output.args.patchText, APPLY_PATCH_RESCUE_OPTIONS, worktree);
4334
+ if (result.changed) {
4335
+ output.args.patchText = result.patchText;
4336
+ logHookStatus("rewrite", {
4337
+ rewrittenChunks: result.rewrittenChunks,
4338
+ totalChunks: result.totalChunks,
4339
+ strategies: result.rewriteModes
4340
+ });
4341
+ return;
4342
+ }
4343
+ logHookStatus("unchanged", {
4344
+ rewrittenChunks: 0,
4345
+ totalChunks: result.totalChunks
4346
+ });
4347
+ return;
4348
+ } catch (error) {
4349
+ const normalizedError = isApplyPatchError(error) ? error : createApplyPatchInternalError(`Unexpected hook failure before native apply: ${error instanceof Error ? error.message : String(error)}`, error);
4350
+ const details = getApplyPatchErrorDetails(normalizedError);
4351
+ logHookStatus(isApplyPatchVerificationError(normalizedError) ? "verification" : normalizedError.kind === "validation" ? "validation" : normalizedError.kind === "internal" ? "internal" : "blocked", {
4352
+ kind: details?.kind ?? "internal",
4353
+ code: details?.code ?? "internal_unexpected",
4354
+ reason: normalizedError.message,
4355
+ failOpen: false,
4356
+ rescueOptions: APPLY_PATCH_RESCUE_OPTIONS,
4357
+ rewriteStage: "before-native"
4358
+ });
4359
+ throw normalizedError;
4360
+ }
4361
+ }
4362
+ };
4363
+ }
2959
4364
  // src/hooks/auto-update-checker/cache.ts
2960
- import * as fs3 from "fs";
2961
- import * as path4 from "path";
4365
+ import * as fs4 from "fs";
4366
+ import * as path6 from "path";
2962
4367
  // src/hooks/auto-update-checker/constants.ts
2963
4368
  import * as os2 from "os";
2964
- import * as path3 from "path";
4369
+ import * as path5 from "path";
2965
4370
  var PACKAGE_NAME = "oh-my-opencode-slim";
2966
4371
  var NPM_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`;
2967
4372
  var NPM_FETCH_TIMEOUT = 5000;
2968
4373
  function getCacheDir() {
2969
4374
  if (process.platform === "win32") {
2970
- return path3.join(process.env.LOCALAPPDATA ?? os2.homedir(), "opencode");
4375
+ return path5.join(process.env.LOCALAPPDATA ?? os2.homedir(), "opencode");
2971
4376
  }
2972
- return path3.join(os2.homedir(), ".cache", "opencode");
4377
+ return path5.join(os2.homedir(), ".cache", "opencode");
2973
4378
  }
2974
4379
  var CACHE_DIR = getCacheDir();
2975
- var INSTALLED_PACKAGE_JSON = path3.join(CACHE_DIR, "node_modules", PACKAGE_NAME, "package.json");
4380
+ var INSTALLED_PACKAGE_JSON = path5.join(CACHE_DIR, "node_modules", PACKAGE_NAME, "package.json");
2976
4381
  var configPaths = getOpenCodeConfigPaths();
2977
4382
  var USER_OPENCODE_CONFIG = configPaths[0];
2978
4383
  var USER_OPENCODE_CONFIG_JSONC = configPaths[1];
2979
4384
 
2980
4385
  // src/hooks/auto-update-checker/cache.ts
2981
4386
  function removeFromBunLock(packageName) {
2982
- const lockPath = path4.join(CACHE_DIR, "bun.lock");
2983
- if (!fs3.existsSync(lockPath))
4387
+ const lockPath = path6.join(CACHE_DIR, "bun.lock");
4388
+ if (!fs4.existsSync(lockPath))
2984
4389
  return false;
2985
4390
  try {
2986
- const content = fs3.readFileSync(lockPath, "utf-8");
4391
+ const content = fs4.readFileSync(lockPath, "utf-8");
2987
4392
  let lock;
2988
4393
  try {
2989
4394
  lock = JSON.parse(stripJsonComments(content));
@@ -3000,7 +4405,7 @@ function removeFromBunLock(packageName) {
3000
4405
  modified = true;
3001
4406
  }
3002
4407
  if (modified) {
3003
- fs3.writeFileSync(lockPath, JSON.stringify(lock, null, 2));
4408
+ fs4.writeFileSync(lockPath, JSON.stringify(lock, null, 2));
3004
4409
  log(`[auto-update-checker] Removed from bun.lock: ${packageName}`);
3005
4410
  }
3006
4411
  return modified;
@@ -3011,23 +4416,23 @@ function removeFromBunLock(packageName) {
3011
4416
  }
3012
4417
  function invalidatePackage(packageName = PACKAGE_NAME) {
3013
4418
  try {
3014
- const pkgDir = path4.join(CACHE_DIR, "node_modules", packageName);
3015
- const pkgJsonPath = path4.join(CACHE_DIR, "package.json");
4419
+ const pkgDir = path6.join(CACHE_DIR, "node_modules", packageName);
4420
+ const pkgJsonPath = path6.join(CACHE_DIR, "package.json");
3016
4421
  let packageRemoved = false;
3017
4422
  let dependencyRemoved = false;
3018
4423
  let lockRemoved = false;
3019
- if (fs3.existsSync(pkgDir)) {
3020
- fs3.rmSync(pkgDir, { recursive: true, force: true });
4424
+ if (fs4.existsSync(pkgDir)) {
4425
+ fs4.rmSync(pkgDir, { recursive: true, force: true });
3021
4426
  log(`[auto-update-checker] Package removed: ${pkgDir}`);
3022
4427
  packageRemoved = true;
3023
4428
  }
3024
- if (fs3.existsSync(pkgJsonPath)) {
4429
+ if (fs4.existsSync(pkgJsonPath)) {
3025
4430
  try {
3026
- const content = fs3.readFileSync(pkgJsonPath, "utf-8");
4431
+ const content = fs4.readFileSync(pkgJsonPath, "utf-8");
3027
4432
  const pkgJson = JSON.parse(stripJsonComments(content));
3028
4433
  if (pkgJson.dependencies?.[packageName]) {
3029
4434
  delete pkgJson.dependencies[packageName];
3030
- fs3.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
4435
+ fs4.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
3031
4436
  log(`[auto-update-checker] Dependency removed from package.json: ${packageName}`);
3032
4437
  dependencyRemoved = true;
3033
4438
  }
@@ -3048,8 +4453,8 @@ function invalidatePackage(packageName = PACKAGE_NAME) {
3048
4453
  }
3049
4454
 
3050
4455
  // src/hooks/auto-update-checker/checker.ts
3051
- import * as fs4 from "fs";
3052
- import * as path5 from "path";
4456
+ import * as fs5 from "fs";
4457
+ import * as path7 from "path";
3053
4458
  import { fileURLToPath } from "url";
3054
4459
  function isPrereleaseVersion(version) {
3055
4460
  return version.includes("-");
@@ -3074,8 +4479,8 @@ function extractChannel(version) {
3074
4479
  }
3075
4480
  function getConfigPaths(directory) {
3076
4481
  return [
3077
- path5.join(directory, ".opencode", "opencode.json"),
3078
- path5.join(directory, ".opencode", "opencode.jsonc"),
4482
+ path7.join(directory, ".opencode", "opencode.json"),
4483
+ path7.join(directory, ".opencode", "opencode.jsonc"),
3079
4484
  USER_OPENCODE_CONFIG,
3080
4485
  USER_OPENCODE_CONFIG_JSONC
3081
4486
  ];
@@ -3083,9 +4488,9 @@ function getConfigPaths(directory) {
3083
4488
  function getLocalDevPath(directory) {
3084
4489
  for (const configPath of getConfigPaths(directory)) {
3085
4490
  try {
3086
- if (!fs4.existsSync(configPath))
4491
+ if (!fs5.existsSync(configPath))
3087
4492
  continue;
3088
- const content = fs4.readFileSync(configPath, "utf-8");
4493
+ const content = fs5.readFileSync(configPath, "utf-8");
3089
4494
  const config = JSON.parse(stripJsonComments(content));
3090
4495
  const plugins = config.plugin ?? [];
3091
4496
  for (const entry of plugins) {
@@ -3103,19 +4508,19 @@ function getLocalDevPath(directory) {
3103
4508
  }
3104
4509
  function findPackageJsonUp(startPath) {
3105
4510
  try {
3106
- const stat = fs4.statSync(startPath);
3107
- let dir = stat.isDirectory() ? startPath : path5.dirname(startPath);
4511
+ const stat2 = fs5.statSync(startPath);
4512
+ let dir = stat2.isDirectory() ? startPath : path7.dirname(startPath);
3108
4513
  for (let i = 0;i < 10; i++) {
3109
- const pkgPath = path5.join(dir, "package.json");
3110
- if (fs4.existsSync(pkgPath)) {
4514
+ const pkgPath = path7.join(dir, "package.json");
4515
+ if (fs5.existsSync(pkgPath)) {
3111
4516
  try {
3112
- const content = fs4.readFileSync(pkgPath, "utf-8");
4517
+ const content = fs5.readFileSync(pkgPath, "utf-8");
3113
4518
  const pkg = JSON.parse(content);
3114
4519
  if (pkg.name === PACKAGE_NAME)
3115
4520
  return pkgPath;
3116
4521
  } catch {}
3117
4522
  }
3118
- const parent = path5.dirname(dir);
4523
+ const parent = path7.dirname(dir);
3119
4524
  if (parent === dir)
3120
4525
  break;
3121
4526
  dir = parent;
@@ -3131,7 +4536,7 @@ function getLocalDevVersion(directory) {
3131
4536
  const pkgPath = findPackageJsonUp(localPath);
3132
4537
  if (!pkgPath)
3133
4538
  return null;
3134
- const content = fs4.readFileSync(pkgPath, "utf-8");
4539
+ const content = fs5.readFileSync(pkgPath, "utf-8");
3135
4540
  const pkg = JSON.parse(content);
3136
4541
  return pkg.version ?? null;
3137
4542
  } catch {
@@ -3141,9 +4546,9 @@ function getLocalDevVersion(directory) {
3141
4546
  function findPluginEntry(directory) {
3142
4547
  for (const configPath of getConfigPaths(directory)) {
3143
4548
  try {
3144
- if (!fs4.existsSync(configPath))
4549
+ if (!fs5.existsSync(configPath))
3145
4550
  continue;
3146
- const content = fs4.readFileSync(configPath, "utf-8");
4551
+ const content = fs5.readFileSync(configPath, "utf-8");
3147
4552
  const config = JSON.parse(stripJsonComments(content));
3148
4553
  const plugins = config.plugin ?? [];
3149
4554
  for (const entry of plugins) {
@@ -3170,8 +4575,8 @@ function getCachedVersion() {
3170
4575
  if (cachedPackageVersion)
3171
4576
  return cachedPackageVersion;
3172
4577
  try {
3173
- if (fs4.existsSync(INSTALLED_PACKAGE_JSON)) {
3174
- const content = fs4.readFileSync(INSTALLED_PACKAGE_JSON, "utf-8");
4578
+ if (fs5.existsSync(INSTALLED_PACKAGE_JSON)) {
4579
+ const content = fs5.readFileSync(INSTALLED_PACKAGE_JSON, "utf-8");
3175
4580
  const pkg = JSON.parse(content);
3176
4581
  if (pkg.version) {
3177
4582
  cachedPackageVersion = pkg.version;
@@ -3180,10 +4585,10 @@ function getCachedVersion() {
3180
4585
  }
3181
4586
  } catch {}
3182
4587
  try {
3183
- const currentDir = path5.dirname(fileURLToPath(import.meta.url));
4588
+ const currentDir = path7.dirname(fileURLToPath(import.meta.url));
3184
4589
  const pkgPath = findPackageJsonUp(currentDir);
3185
4590
  if (pkgPath) {
3186
- const content = fs4.readFileSync(pkgPath, "utf-8");
4591
+ const content = fs5.readFileSync(pkgPath, "utf-8");
3187
4592
  const pkg = JSON.parse(content);
3188
4593
  if (pkg.version) {
3189
4594
  cachedPackageVersion = pkg.version;
@@ -3858,17 +5263,35 @@ ${originalText}`;
3858
5263
  };
3859
5264
  }
3860
5265
  // src/hooks/post-file-tool-nudge/index.ts
3861
- var NUDGE = `
3862
-
3863
- ---
3864
- ${PHASE_REMINDER_TEXT}`;
3865
- function createPostFileToolNudgeHook() {
5266
+ var POST_FILE_TOOL_NUDGE = PHASE_REMINDER_TEXT;
5267
+ var FILE_TOOLS = new Set(["Read", "read", "Write", "write"]);
5268
+ function createPostFileToolNudgeHook(options = {}) {
5269
+ const pendingSessionIds = new Set;
3866
5270
  return {
3867
- "tool.execute.after": async (input, output) => {
3868
- if (input.tool !== "Read" && input.tool !== "read" && input.tool !== "Write" && input.tool !== "write") {
5271
+ "tool.execute.after": async (input, _output) => {
5272
+ if (!FILE_TOOLS.has(input.tool) || !input.sessionID) {
5273
+ return;
5274
+ }
5275
+ pendingSessionIds.add(input.sessionID);
5276
+ },
5277
+ "experimental.chat.system.transform": async (input, output) => {
5278
+ if (!input.sessionID || !pendingSessionIds.delete(input.sessionID)) {
3869
5279
  return;
3870
5280
  }
3871
- output.output = output.output + NUDGE;
5281
+ if (options.shouldInject && !options.shouldInject(input.sessionID)) {
5282
+ return;
5283
+ }
5284
+ output.system.push(POST_FILE_TOOL_NUDGE);
5285
+ },
5286
+ event: async (input) => {
5287
+ if (input.event.type !== "session.deleted") {
5288
+ return;
5289
+ }
5290
+ const sessionID = input.event.properties?.sessionID ?? input.event.properties?.info?.id;
5291
+ if (!sessionID) {
5292
+ return;
5293
+ }
5294
+ pendingSessionIds.delete(sessionID);
3872
5295
  }
3873
5296
  };
3874
5297
  }
@@ -3878,6 +5301,7 @@ var HOOK_NAME = "todo-continuation";
3878
5301
  var COMMAND_NAME = "auto-continue";
3879
5302
  var CONTINUATION_PROMPT = "[Auto-continue: enabled - there are incomplete todos remaining. Continue with the next uncompleted item. Press Esc to cancel. If you need user input or review for the next item, ask instead of proceeding.]";
3880
5303
  var SUPPRESS_AFTER_ABORT_MS = 5000;
5304
+ var NOTIFICATION_BUSY_GRACE_MS = 250;
3881
5305
  var QUESTION_PHRASES = [
3882
5306
  "would you like",
3883
5307
  "should i",
@@ -3903,12 +5327,15 @@ function cancelPendingTimer(state) {
3903
5327
  clearTimeout(state.pendingTimer);
3904
5328
  state.pendingTimer = null;
3905
5329
  }
5330
+ state.pendingTimerSessionId = null;
3906
5331
  }
3907
5332
  function resetState(state) {
3908
5333
  cancelPendingTimer(state);
3909
5334
  state.consecutiveContinuations = 0;
3910
5335
  state.suppressUntil = 0;
3911
5336
  state.isAutoInjecting = false;
5337
+ state.notifyingSessionIds.clear();
5338
+ state.notificationBusyUntilBySession.clear();
3912
5339
  }
3913
5340
  function createTodoContinuationHook(ctx, config) {
3914
5341
  const maxContinuations = config?.maxContinuations ?? 5;
@@ -3919,10 +5346,51 @@ function createTodoContinuationHook(ctx, config) {
3919
5346
  enabled: false,
3920
5347
  consecutiveContinuations: 0,
3921
5348
  pendingTimer: null,
5349
+ pendingTimerSessionId: null,
3922
5350
  suppressUntil: 0,
3923
- orchestratorSessionId: null,
3924
- isAutoInjecting: false
5351
+ orchestratorSessionIds: new Set,
5352
+ sawChatMessage: false,
5353
+ isAutoInjecting: false,
5354
+ notifyingSessionIds: new Set,
5355
+ notificationBusyUntilBySession: new Map
3925
5356
  };
5357
+ function markNotificationStarted(sessionID) {
5358
+ state.notifyingSessionIds.add(sessionID);
5359
+ }
5360
+ function markNotificationFinished(sessionID) {
5361
+ state.notifyingSessionIds.delete(sessionID);
5362
+ state.notificationBusyUntilBySession.set(sessionID, Date.now() + NOTIFICATION_BUSY_GRACE_MS);
5363
+ }
5364
+ function clearNotificationState(sessionID) {
5365
+ state.notifyingSessionIds.delete(sessionID);
5366
+ state.notificationBusyUntilBySession.delete(sessionID);
5367
+ }
5368
+ function isNotificationBusy(sessionID) {
5369
+ if (state.notifyingSessionIds.has(sessionID)) {
5370
+ return true;
5371
+ }
5372
+ const until = state.notificationBusyUntilBySession.get(sessionID) ?? 0;
5373
+ if (until <= Date.now()) {
5374
+ state.notificationBusyUntilBySession.delete(sessionID);
5375
+ return false;
5376
+ }
5377
+ return true;
5378
+ }
5379
+ function isOrchestratorSession(sessionID) {
5380
+ return state.orchestratorSessionIds.has(sessionID);
5381
+ }
5382
+ function registerOrchestratorSession(sessionID) {
5383
+ state.orchestratorSessionIds.add(sessionID);
5384
+ }
5385
+ function handleChatMessage(input) {
5386
+ if (!input.agent) {
5387
+ return;
5388
+ }
5389
+ state.sawChatMessage = true;
5390
+ if (input.agent === "orchestrator") {
5391
+ registerOrchestratorSession(input.sessionID);
5392
+ }
5393
+ }
3926
5394
  const autoContinue = tool({
3927
5395
  description: "Toggle auto-continuation for incomplete todos. When enabled, the orchestrator will automatically continue working through its todo list when it stops with incomplete items.",
3928
5396
  args: { enabled: tool.schema.boolean() },
@@ -3943,19 +5411,19 @@ function createTodoContinuationHook(ctx, config) {
3943
5411
  async function handleEvent(input) {
3944
5412
  const { event } = input;
3945
5413
  const properties = event.properties ?? {};
3946
- if (event.type === "session.idle") {
5414
+ if (event.type === "session.idle" || event.type === "session.status" && properties.status?.type === "idle") {
3947
5415
  const sessionID = properties.sessionID;
3948
5416
  if (!sessionID) {
3949
5417
  return;
3950
5418
  }
3951
5419
  log(`[${HOOK_NAME}] Session idle`, { sessionID });
3952
- if (!state.orchestratorSessionId) {
3953
- state.orchestratorSessionId = sessionID;
5420
+ if (!state.sawChatMessage && state.orchestratorSessionIds.size === 0) {
5421
+ registerOrchestratorSession(sessionID);
3954
5422
  log(`[${HOOK_NAME}] Tracked orchestrator session`, {
3955
5423
  sessionID
3956
5424
  });
3957
5425
  }
3958
- if (state.orchestratorSessionId !== sessionID) {
5426
+ if (!isOrchestratorSession(sessionID)) {
3959
5427
  log(`[${HOOK_NAME}] Skipped: not orchestrator session`, {
3960
5428
  sessionID
3961
5429
  });
@@ -4068,6 +5536,7 @@ function createTodoContinuationHook(ctx, config) {
4068
5536
  sessionID,
4069
5537
  delayMs: cooldownMs
4070
5538
  });
5539
+ markNotificationStarted(sessionID);
4071
5540
  ctx.client.session.prompt({
4072
5541
  path: { id: sessionID },
4073
5542
  body: {
@@ -4084,9 +5553,14 @@ function createTodoContinuationHook(ctx, config) {
4084
5553
  }
4085
5554
  ]
4086
5555
  }
4087
- }).catch(() => {});
5556
+ }).catch(() => {}).finally(() => {
5557
+ markNotificationFinished(sessionID);
5558
+ });
5559
+ state.pendingTimerSessionId = sessionID;
4088
5560
  state.pendingTimer = setTimeout(async () => {
4089
5561
  state.pendingTimer = null;
5562
+ state.pendingTimerSessionId = null;
5563
+ clearNotificationState(sessionID);
4090
5564
  if (!state.enabled) {
4091
5565
  log(`[${HOOK_NAME}] Cancelled: disabled during cooldown`, {
4092
5566
  sessionID
@@ -4119,11 +5593,12 @@ function createTodoContinuationHook(ctx, config) {
4119
5593
  const status = properties.status;
4120
5594
  const sessionID = properties.sessionID;
4121
5595
  if (status?.type === "busy") {
4122
- const isOrchestrator = sessionID === state.orchestratorSessionId;
4123
- if (isOrchestrator) {
5596
+ const isOrchestrator = isOrchestratorSession(sessionID);
5597
+ const isNotification = isNotificationBusy(sessionID);
5598
+ if (isOrchestrator && !isNotification && state.pendingTimerSessionId === sessionID) {
4124
5599
  cancelPendingTimer(state);
4125
5600
  }
4126
- if (!state.isAutoInjecting && isOrchestrator && state.consecutiveContinuations > 0) {
5601
+ if (!state.isAutoInjecting && !isNotification && isOrchestrator && state.consecutiveContinuations > 0) {
4127
5602
  state.consecutiveContinuations = 0;
4128
5603
  log(`[${HOOK_NAME}] Reset consecutive count on user activity`, {
4129
5604
  sessionID
@@ -4134,7 +5609,7 @@ function createTodoContinuationHook(ctx, config) {
4134
5609
  const error = properties.error;
4135
5610
  const sessionID = properties.sessionID;
4136
5611
  const errorName = error?.name;
4137
- const isOrchestrator = sessionID === state.orchestratorSessionId;
5612
+ const isOrchestrator = isOrchestratorSession(sessionID);
4138
5613
  if (isOrchestrator && (errorName === "MessageAbortedError" || errorName === "AbortError")) {
4139
5614
  state.suppressUntil = Date.now() + SUPPRESS_AFTER_ABORT_MS;
4140
5615
  log(`[${HOOK_NAME}] Suppressed continuation after abort`, {
@@ -4150,13 +5625,19 @@ function createTodoContinuationHook(ctx, config) {
4150
5625
  }
4151
5626
  } else if (event.type === "session.deleted") {
4152
5627
  const deletedSessionId = properties.info?.id ?? properties.sessionID;
4153
- if (state.orchestratorSessionId === deletedSessionId) {
4154
- cancelPendingTimer(state);
4155
- log(`[${HOOK_NAME}] Cancelled pending timer on orchestrator delete`, {
4156
- sessionID: deletedSessionId
4157
- });
4158
- resetState(state);
4159
- state.orchestratorSessionId = null;
5628
+ if (deletedSessionId && isOrchestratorSession(deletedSessionId)) {
5629
+ if (state.pendingTimerSessionId === deletedSessionId) {
5630
+ cancelPendingTimer(state);
5631
+ log(`[${HOOK_NAME}] Cancelled pending timer on orchestrator delete`, {
5632
+ sessionID: deletedSessionId
5633
+ });
5634
+ }
5635
+ state.orchestratorSessionIds.delete(deletedSessionId);
5636
+ clearNotificationState(deletedSessionId);
5637
+ if (state.orchestratorSessionIds.size === 0) {
5638
+ resetState(state);
5639
+ state.sawChatMessage = false;
5640
+ }
4160
5641
  log(`[${HOOK_NAME}] Reset orchestrator session on delete`, {
4161
5642
  sessionID: deletedSessionId
4162
5643
  });
@@ -4167,9 +5648,7 @@ function createTodoContinuationHook(ctx, config) {
4167
5648
  if (input.command !== COMMAND_NAME) {
4168
5649
  return;
4169
5650
  }
4170
- if (!state.orchestratorSessionId) {
4171
- state.orchestratorSessionId = input.sessionID;
4172
- }
5651
+ registerOrchestratorSession(input.sessionID);
4173
5652
  output.parts.length = 0;
4174
5653
  const arg = input.arguments.trim().toLowerCase();
4175
5654
  let newEnabled;
@@ -4214,6 +5693,7 @@ function createTodoContinuationHook(ctx, config) {
4214
5693
  return {
4215
5694
  tool: { auto_continue: autoContinue },
4216
5695
  handleEvent,
5696
+ handleChatMessage,
4217
5697
  handleCommandExecuteBefore
4218
5698
  };
4219
5699
  }
@@ -5077,8 +6557,8 @@ function createInterviewServer(deps) {
5077
6557
  // src/interview/service.ts
5078
6558
  import { spawn as spawn4 } from "child_process";
5079
6559
  import * as fsSync from "fs";
5080
- import * as fs5 from "fs/promises";
5081
- import * as path6 from "path";
6560
+ import * as fs6 from "fs/promises";
6561
+ import * as path8 from "path";
5082
6562
 
5083
6563
  // src/interview/parser.ts
5084
6564
  var INTERVIEW_BLOCK_REGEX = /<interview_state>\s*([\s\S]*?)\s*<\/interview_state>/i;
@@ -5271,14 +6751,14 @@ function normalizeOutputFolder(outputFolder) {
5271
6751
  return normalized || DEFAULT_OUTPUT_FOLDER;
5272
6752
  }
5273
6753
  function createInterviewDirectoryPath(directory, outputFolder) {
5274
- return path6.join(directory, normalizeOutputFolder(outputFolder));
6754
+ return path8.join(directory, normalizeOutputFolder(outputFolder));
5275
6755
  }
5276
6756
  function createInterviewFilePath(directory, outputFolder, idea) {
5277
6757
  const fileName = `${slugify(idea) || "interview"}.md`;
5278
- return path6.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
6758
+ return path8.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
5279
6759
  }
5280
6760
  function relativeInterviewPath(directory, filePath) {
5281
- return path6.relative(directory, filePath) || path6.basename(filePath);
6761
+ return path8.relative(directory, filePath) || path8.basename(filePath);
5282
6762
  }
5283
6763
  function extractHistorySection(document) {
5284
6764
  const marker = `## Q&A history
@@ -5324,31 +6804,31 @@ function buildInterviewDocument(idea, summary, history) {
5324
6804
  `);
5325
6805
  }
5326
6806
  async function ensureInterviewFile(record) {
5327
- await fs5.mkdir(path6.dirname(record.markdownPath), { recursive: true });
6807
+ await fs6.mkdir(path8.dirname(record.markdownPath), { recursive: true });
5328
6808
  try {
5329
- await fs5.access(record.markdownPath);
6809
+ await fs6.access(record.markdownPath);
5330
6810
  } catch {
5331
- await fs5.writeFile(record.markdownPath, buildInterviewDocument(record.idea, "", ""), "utf8");
6811
+ await fs6.writeFile(record.markdownPath, buildInterviewDocument(record.idea, "", ""), "utf8");
5332
6812
  }
5333
6813
  }
5334
6814
  async function readInterviewDocument(record) {
5335
6815
  try {
5336
- return await fs5.readFile(record.markdownPath, "utf8");
6816
+ return await fs6.readFile(record.markdownPath, "utf8");
5337
6817
  } catch (error) {
5338
6818
  if (error.code === "ENOENT") {
5339
6819
  try {
5340
- return await fs5.readFile(record.markdownPath, "utf8");
6820
+ return await fs6.readFile(record.markdownPath, "utf8");
5341
6821
  } catch {}
5342
6822
  }
5343
6823
  }
5344
6824
  await ensureInterviewFile(record);
5345
- return fs5.readFile(record.markdownPath, "utf8");
6825
+ return fs6.readFile(record.markdownPath, "utf8");
5346
6826
  }
5347
6827
  async function rewriteInterviewDocument(record, summary) {
5348
6828
  const existing = await readInterviewDocument(record);
5349
6829
  const history = extractHistorySection(existing);
5350
6830
  const next = buildInterviewDocument(record.idea, summary, history);
5351
- await fs5.writeFile(record.markdownPath, next, "utf8");
6831
+ await fs6.writeFile(record.markdownPath, next, "utf8");
5352
6832
  return next;
5353
6833
  }
5354
6834
  async function appendInterviewAnswers(record, questions, answers) {
@@ -5366,7 +6846,7 @@ A: ${answer.answer.trim()}` : null;
5366
6846
  const nextHistory = [history === "No answers yet." ? "" : history, appended].filter(Boolean).join(`
5367
6847
 
5368
6848
  `);
5369
- await fs5.writeFile(record.markdownPath, buildInterviewDocument(record.idea, summary, nextHistory), "utf8");
6849
+ await fs6.writeFile(record.markdownPath, buildInterviewDocument(record.idea, summary, nextHistory), "utf8");
5370
6850
  }
5371
6851
  function resolveExistingInterviewPath(directory, outputFolder, value) {
5372
6852
  const trimmed = value.trim();
@@ -5375,22 +6855,22 @@ function resolveExistingInterviewPath(directory, outputFolder, value) {
5375
6855
  }
5376
6856
  const outputDir = createInterviewDirectoryPath(directory, outputFolder);
5377
6857
  const candidates = new Set;
5378
- const resolvedRoot = path6.resolve(directory);
5379
- if (path6.isAbsolute(trimmed)) {
6858
+ const resolvedRoot = path8.resolve(directory);
6859
+ if (path8.isAbsolute(trimmed)) {
5380
6860
  candidates.add(trimmed);
5381
6861
  } else {
5382
- candidates.add(path6.resolve(directory, trimmed));
5383
- candidates.add(path6.join(outputDir, trimmed));
6862
+ candidates.add(path8.resolve(directory, trimmed));
6863
+ candidates.add(path8.join(outputDir, trimmed));
5384
6864
  if (!trimmed.endsWith(".md")) {
5385
- candidates.add(path6.join(outputDir, `${trimmed}.md`));
6865
+ candidates.add(path8.join(outputDir, `${trimmed}.md`));
5386
6866
  }
5387
6867
  }
5388
6868
  for (const candidate of candidates) {
5389
- if (path6.extname(candidate) !== ".md") {
6869
+ if (path8.extname(candidate) !== ".md") {
5390
6870
  continue;
5391
6871
  }
5392
- const resolved = path6.resolve(candidate);
5393
- if (!resolved.startsWith(resolvedRoot + path6.sep) && resolved !== resolvedRoot) {
6872
+ const resolved = path8.resolve(candidate);
6873
+ if (!resolved.startsWith(resolvedRoot + path8.sep) && resolved !== resolvedRoot) {
5394
6874
  continue;
5395
6875
  }
5396
6876
  if (fsSync.existsSync(candidate)) {
@@ -5437,18 +6917,18 @@ function createInterviewService(ctx, config, deps) {
5437
6917
  if (!newSlug) {
5438
6918
  return;
5439
6919
  }
5440
- const currentFileName = path6.basename(interview.markdownPath, ".md");
6920
+ const currentFileName = path8.basename(interview.markdownPath, ".md");
5441
6921
  if (currentFileName === newSlug) {
5442
6922
  return;
5443
6923
  }
5444
- const dir = path6.dirname(interview.markdownPath);
5445
- const newPath = path6.join(dir, `${newSlug}.md`);
6924
+ const dir = path8.dirname(interview.markdownPath);
6925
+ const newPath = path8.join(dir, `${newSlug}.md`);
5446
6926
  try {
5447
- await fs5.access(newPath);
6927
+ await fs6.access(newPath);
5448
6928
  return;
5449
6929
  } catch {}
5450
6930
  try {
5451
- await fs5.rename(interview.markdownPath, newPath);
6931
+ await fs6.rename(interview.markdownPath, newPath);
5452
6932
  interview.markdownPath = newPath;
5453
6933
  log("[interview] renamed file with assistant title:", {
5454
6934
  from: currentFileName,
@@ -5510,13 +6990,13 @@ function createInterviewService(ctx, config, deps) {
5510
6990
  active.status = "abandoned";
5511
6991
  }
5512
6992
  }
5513
- const document = await fs5.readFile(markdownPath, "utf8");
6993
+ const document = await fs6.readFile(markdownPath, "utf8");
5514
6994
  const messages = await loadMessages(sessionID);
5515
6995
  const title = extractTitle(document);
5516
6996
  const record = {
5517
- id: `${Date.now()}-${++idCounter}-${slugify(path6.basename(markdownPath, ".md")) || "interview"}`,
6997
+ id: `${Date.now()}-${++idCounter}-${slugify(path8.basename(markdownPath, ".md")) || "interview"}`,
5518
6998
  sessionID,
5519
- idea: title || path6.basename(markdownPath, ".md"),
6999
+ idea: title || path8.basename(markdownPath, ".md"),
5520
7000
  markdownPath,
5521
7001
  createdAt: nowIso(),
5522
7002
  status: "active",
@@ -5658,7 +7138,7 @@ function createInterviewService(ctx, config, deps) {
5658
7138
  const resumePath = resolveExistingInterviewPath(ctx.directory, outputFolder, idea);
5659
7139
  if (resumePath) {
5660
7140
  const interview2 = await resumeInterview(input.sessionID, resumePath);
5661
- const document = await fs5.readFile(interview2.markdownPath, "utf8");
7141
+ const document = await fs6.readFile(interview2.markdownPath, "utf8");
5662
7142
  await notifyInterviewUrl(input.sessionID, interview2);
5663
7143
  output.parts.push(createInternalAgentTextPart(buildResumePrompt(document, maxQuestions)));
5664
7144
  return;
@@ -5969,9 +7449,9 @@ function findSgCliPathSync() {
5969
7449
  }
5970
7450
  if (process.platform === "darwin") {
5971
7451
  const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
5972
- for (const path7 of homebrewPaths) {
5973
- if (existsSync6(path7) && isValidBinary(path7)) {
5974
- return path7;
7452
+ for (const path9 of homebrewPaths) {
7453
+ if (existsSync6(path9) && isValidBinary(path9)) {
7454
+ return path9;
5975
7455
  }
5976
7456
  }
5977
7457
  }
@@ -5988,8 +7468,8 @@ function getSgCliPath() {
5988
7468
  }
5989
7469
  return "sg";
5990
7470
  }
5991
- function setSgCliPath(path7) {
5992
- resolvedCliPath = path7;
7471
+ function setSgCliPath(path9) {
7472
+ resolvedCliPath = path9;
5993
7473
  }
5994
7474
  var DEFAULT_TIMEOUT_MS2 = 300000;
5995
7475
  var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
@@ -8232,14 +9712,14 @@ var BINARY_PREFIXES = [
8232
9712
  var WEBFETCH_DESCRIPTION = "Fetch a URL with better extraction for static/docs pages. Supports llms.txt probing, content-focused HTML extraction, metadata, redirects, and an optional prompt processed by a cheap secondary model.";
8233
9713
  // src/tools/smartfetch/tool.ts
8234
9714
  import os3 from "os";
8235
- import path10 from "path";
9715
+ import path12 from "path";
8236
9716
  import {
8237
9717
  tool as tool6
8238
9718
  } from "@opencode-ai/plugin";
8239
9719
 
8240
9720
  // src/tools/smartfetch/binary.ts
8241
9721
  import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
8242
- import path7 from "path";
9722
+ import path9 from "path";
8243
9723
  function extensionForMime(contentType) {
8244
9724
  const mime = contentType.split(";")[0]?.trim().toLowerCase();
8245
9725
  const map = {
@@ -8260,10 +9740,10 @@ function buildBinaryResultMessage(fetchResult, savedPath) {
8260
9740
  async function saveBinary(binaryDir, data, contentType, filename) {
8261
9741
  await mkdir2(binaryDir, { recursive: true });
8262
9742
  const initialName = filename || `webfetch-${Date.now()}.${extensionForMime(contentType)}`;
8263
- const parsed = path7.parse(initialName);
9743
+ const parsed = path9.parse(initialName);
8264
9744
  for (let attempt = 0;attempt < 1000; attempt++) {
8265
9745
  const candidateName = attempt === 0 ? initialName : `${parsed.name}-${attempt}${parsed.ext || `.${extensionForMime(contentType)}`}`;
8266
- const file = path7.join(binaryDir, candidateName);
9746
+ const file = path9.join(binaryDir, candidateName);
8267
9747
  try {
8268
9748
  await writeFile2(file, data, { flag: "wx" });
8269
9749
  return file;
@@ -8281,7 +9761,7 @@ async function saveBinary(binaryDir, data, contentType, filename) {
8281
9761
  import { LRUCache } from "lru-cache";
8282
9762
 
8283
9763
  // src/tools/smartfetch/network.ts
8284
- import path8 from "path";
9764
+ import path10 from "path";
8285
9765
 
8286
9766
  // src/tools/smartfetch/utils.ts
8287
9767
  import { Readability } from "@mozilla/readability";
@@ -8665,7 +10145,7 @@ function normalizeUrl(input) {
8665
10145
  }
8666
10146
  function isDocsLikeUrl(url) {
8667
10147
  const host = url.hostname.toLowerCase();
8668
- return DOCS_HOST_SUFFIXES.some((suffix) => host.endsWith(suffix)) || DOCS_HOST_PREFIXES.some((prefix) => host.startsWith(prefix));
10148
+ return DOCS_HOST_SUFFIXES.some((suffix2) => host.endsWith(suffix2)) || DOCS_HOST_PREFIXES.some((prefix2) => host.startsWith(prefix2));
8669
10149
  }
8670
10150
  function buildPermissionPatterns(normalized, shouldProbeLlmsTxt) {
8671
10151
  const patterns = new Set([normalized.url]);
@@ -8721,7 +10201,7 @@ function isPermittedRedirect(from, to, allowedOrigins) {
8721
10201
  }
8722
10202
  function isBinaryContentType(contentType) {
8723
10203
  const mime = contentType.split(";")[0]?.trim().toLowerCase() || "";
8724
- return BINARY_PREFIXES.some((prefix) => mime.startsWith(prefix));
10204
+ return BINARY_PREFIXES.some((prefix2) => mime.startsWith(prefix2));
8725
10205
  }
8726
10206
  function getBinaryKind(contentType) {
8727
10207
  const mime = contentType.split(";")[0]?.trim().toLowerCase() || "";
@@ -9000,7 +10480,7 @@ function inferFilenameFromUrl(url) {
9000
10480
  function truncateFilename(name, maxLength = 180) {
9001
10481
  if (name.length <= maxLength)
9002
10482
  return name;
9003
- const parsed = path8.parse(name);
10483
+ const parsed = path10.parse(name);
9004
10484
  const ext = parsed.ext || "";
9005
10485
  const baseLimit = Math.max(1, maxLength - ext.length);
9006
10486
  return `${parsed.name.slice(0, baseLimit)}${ext}`;
@@ -9171,8 +10651,8 @@ function isInvalidLlmsResult(fetchResult) {
9171
10651
 
9172
10652
  // src/tools/smartfetch/secondary-model.ts
9173
10653
  import { existsSync as existsSync11 } from "fs";
9174
- import { readFile as readFile2 } from "fs/promises";
9175
- import path9 from "path";
10654
+ import { readFile as readFile3 } from "fs/promises";
10655
+ import path11 from "path";
9176
10656
  function parseModelRef(value) {
9177
10657
  if (!value)
9178
10658
  return;
@@ -9198,7 +10678,7 @@ function pickAgentModelRef(value) {
9198
10678
  }
9199
10679
  function findPreferredOpenCodeConfigPath(baseDir) {
9200
10680
  for (const file of ["opencode.jsonc", "opencode.json"]) {
9201
- const fullPath = path9.join(baseDir, file);
10681
+ const fullPath = path11.join(baseDir, file);
9202
10682
  if (existsSync11(fullPath))
9203
10683
  return fullPath;
9204
10684
  }
@@ -9208,14 +10688,14 @@ async function readOpenCodeConfigFile(configPath) {
9208
10688
  if (!configPath)
9209
10689
  return;
9210
10690
  try {
9211
- const content = await readFile2(configPath, "utf8");
10691
+ const content = await readFile3(configPath, "utf8");
9212
10692
  return JSON.parse(stripJsonComments(content));
9213
10693
  } catch {
9214
10694
  return;
9215
10695
  }
9216
10696
  }
9217
10697
  async function readEffectiveOpenCodeConfig(directory) {
9218
- const projectDir = path9.join(directory, ".opencode");
10698
+ const projectDir = path11.join(directory, ".opencode");
9219
10699
  const userDirs = getConfigSearchDirs();
9220
10700
  const projectPath = findPreferredOpenCodeConfigPath(projectDir);
9221
10701
  const userPath = userDirs.map((configDir) => findPreferredOpenCodeConfigPath(configDir)).find(Boolean);
@@ -9376,7 +10856,7 @@ async function runSecondaryModelWithFallback(client, directory, models, prompt,
9376
10856
  // src/tools/smartfetch/tool.ts
9377
10857
  var z5 = tool6.schema;
9378
10858
  function createWebfetchTool(pluginCtx, options = {}) {
9379
- const binaryDir = options.binaryDir || path10.join(os3.tmpdir(), "opencode-smartfetch");
10859
+ const binaryDir = options.binaryDir || path12.join(os3.tmpdir(), "opencode-smartfetch");
9380
10860
  return tool6({
9381
10861
  description: WEBFETCH_DESCRIPTION,
9382
10862
  args: {
@@ -9917,12 +11397,15 @@ var OhMyOpenCodeLite = async (ctx) => {
9917
11397
  });
9918
11398
  const phaseReminderHook = createPhaseReminderHook();
9919
11399
  const filterAvailableSkillsHook = createFilterAvailableSkillsHook(ctx, config);
9920
- const postFileToolNudgeHook = createPostFileToolNudgeHook();
11400
+ const sessionAgentMap = new Map;
11401
+ const postFileToolNudgeHook = createPostFileToolNudgeHook({
11402
+ shouldInject: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator"
11403
+ });
9921
11404
  const chatHeadersHook = createChatHeadersHook(ctx);
9922
11405
  const delegateTaskRetryHook = createDelegateTaskRetryHook(ctx);
11406
+ const applyPatchHook = createApplyPatchHook(ctx);
9923
11407
  const jsonErrorRecoveryHook = createJsonErrorRecoveryHook(ctx);
9924
11408
  const foregroundFallback = new ForegroundFallbackManager(ctx.client, runtimeChains, config.fallback?.enabled !== false && Object.keys(runtimeChains).length > 0);
9925
- const sessionAgentMap = new Map;
9926
11409
  const todoContinuationHook = createTodoContinuationHook(ctx, {
9927
11410
  maxContinuations: config.todoContinuation?.maxContinuations ?? 5,
9928
11411
  cooldownMs: config.todoContinuation?.cooldownMs ?? 3000,
@@ -10061,16 +11544,25 @@ var OhMyOpenCodeLite = async (ctx) => {
10061
11544
  await backgroundManager.handleSessionDeleted(input.event);
10062
11545
  await multiplexerSessionManager.onSessionDeleted(input.event);
10063
11546
  await interviewManager.handleEvent(input);
11547
+ await postFileToolNudgeHook.event(input);
11548
+ },
11549
+ "tool.execute.before": async (input, output) => {
11550
+ await applyPatchHook["tool.execute.before"](input, output);
10064
11551
  },
10065
11552
  "command.execute.before": async (input, output) => {
10066
11553
  await todoContinuationHook.handleCommandExecuteBefore(input, output);
10067
11554
  await interviewManager.handleCommandExecuteBefore(input, output);
10068
11555
  },
10069
11556
  "chat.headers": chatHeadersHook["chat.headers"],
10070
- "chat.message": async (input) => {
10071
- if (input.agent) {
10072
- sessionAgentMap.set(input.sessionID, input.agent);
11557
+ "chat.message": async (input, output) => {
11558
+ const agent = input.agent ?? output?.message?.agent;
11559
+ if (agent) {
11560
+ sessionAgentMap.set(input.sessionID, agent);
10073
11561
  }
11562
+ todoContinuationHook.handleChatMessage({
11563
+ sessionID: input.sessionID,
11564
+ agent
11565
+ });
10074
11566
  },
10075
11567
  "experimental.chat.system.transform": async (input, output) => {
10076
11568
  const agentName = input.sessionID ? sessionAgentMap.get(input.sessionID) : undefined;
@@ -10080,9 +11572,10 @@ var OhMyOpenCodeLite = async (ctx) => {
10080
11572
  const { ORCHESTRATOR_PROMPT: ORCHESTRATOR_PROMPT2 } = await Promise.resolve().then(() => exports_orchestrator);
10081
11573
  output.system[0] = ORCHESTRATOR_PROMPT2 + (output.system[0] ? `
10082
11574
 
10083
- ` + output.system[0] : "");
11575
+ ${output.system[0]}` : "");
10084
11576
  }
10085
11577
  }
11578
+ await postFileToolNudgeHook["experimental.chat.system.transform"](input, output);
10086
11579
  },
10087
11580
  "experimental.chat.messages.transform": async (input, output) => {
10088
11581
  const typedOutput = output;