@lannguyensi/harness 0.60.0 → 0.62.0
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/CHANGELOG.md +64 -0
- package/README.md +1 -1
- package/dist/cli/apply/install-codex-config.d.ts +24 -7
- package/dist/cli/apply/install-codex-config.js +353 -81
- package/dist/cli/apply/install-codex-config.js.map +1 -1
- package/dist/cli/approve/understanding.d.ts +31 -6
- package/dist/cli/approve/understanding.js +197 -56
- package/dist/cli/approve/understanding.js.map +1 -1
- package/dist/cli/bounded-stdin.d.ts +40 -0
- package/dist/cli/bounded-stdin.js +111 -0
- package/dist/cli/bounded-stdin.js.map +1 -0
- package/dist/cli/doctor/settings-drift.js +12 -2
- package/dist/cli/doctor/settings-drift.js.map +1 -1
- package/dist/cli/gc/index.d.ts +6 -2
- package/dist/cli/gc/index.js +22 -7
- package/dist/cli/gc/index.js.map +1 -1
- package/dist/cli/index.js +15 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/pack/auto-approve-path.js +39 -4
- package/dist/cli/pack/auto-approve-path.js.map +1 -1
- package/dist/cli/pack/hook-bootstrap.d.ts +59 -5
- package/dist/cli/pack/hook-bootstrap.js +67 -19
- package/dist/cli/pack/hook-bootstrap.js.map +1 -1
- package/dist/cli/pack/hook-branch-protection.d.ts +6 -0
- package/dist/cli/pack/hook-branch-protection.js +11 -2
- package/dist/cli/pack/hook-branch-protection.js.map +1 -1
- package/dist/cli/pack/hook-codex-pre-tool-use.d.ts +6 -0
- package/dist/cli/pack/hook-codex-pre-tool-use.js +20 -2
- package/dist/cli/pack/hook-codex-pre-tool-use.js.map +1 -1
- package/dist/cli/pack/hook-post-merge-gate.d.ts +6 -0
- package/dist/cli/pack/hook-post-merge-gate.js +17 -2
- package/dist/cli/pack/hook-post-merge-gate.js.map +1 -1
- package/dist/cli/pack/hook-pre-tool-use.d.ts +7 -0
- package/dist/cli/pack/hook-pre-tool-use.js +26 -4
- package/dist/cli/pack/hook-pre-tool-use.js.map +1 -1
- package/dist/cli/pack/hook-runtime-reality.d.ts +2 -0
- package/dist/cli/pack/hook-runtime-reality.js +48 -8
- package/dist/cli/pack/hook-runtime-reality.js.map +1 -1
- package/dist/cli/pack/hook-solution-acceptance-writeguard.d.ts +6 -0
- package/dist/cli/pack/hook-solution-acceptance-writeguard.js +11 -2
- package/dist/cli/pack/hook-solution-acceptance-writeguard.js.map +1 -1
- package/dist/cli/pack/hook-solution-acceptance.d.ts +6 -0
- package/dist/cli/pack/hook-solution-acceptance.js +11 -2
- package/dist/cli/pack/hook-solution-acceptance.js.map +1 -1
- package/dist/cli/pack/hook-subagent-start.d.ts +2 -0
- package/dist/cli/pack/hook-subagent-start.js +21 -4
- package/dist/cli/pack/hook-subagent-start.js.map +1 -1
- package/dist/cli/policy/intercept.d.ts +2 -0
- package/dist/cli/policy/intercept.js +57 -21
- package/dist/cli/policy/intercept.js.map +1 -1
- package/dist/cli/record/index.js +97 -21
- package/dist/cli/record/index.js.map +1 -1
- package/dist/cli/session-start/branch-check.js +4 -1
- package/dist/cli/session-start/branch-check.js.map +1 -1
- package/dist/cli/session-start/index.d.ts +0 -15
- package/dist/cli/session-start/index.js +4 -62
- package/dist/cli/session-start/index.js.map +1 -1
- package/dist/cli/session-start/shared-options.d.ts +15 -4
- package/dist/cli/session-start/shared-options.js +13 -15
- package/dist/cli/session-start/shared-options.js.map +1 -1
- package/dist/cli/session-start/stale-base-check.js +4 -1
- package/dist/cli/session-start/stale-base-check.js.map +1 -1
- package/dist/cli/session-start/toolchain-parity.js +4 -1
- package/dist/cli/session-start/toolchain-parity.js.map +1 -1
- package/dist/io/atomic-write.d.ts +37 -1
- package/dist/io/atomic-write.js +61 -8
- package/dist/io/atomic-write.js.map +1 -1
- package/dist/io/display-path.d.ts +11 -0
- package/dist/io/display-path.js +29 -0
- package/dist/io/display-path.js.map +1 -0
- package/dist/policy-packs/builtin/post-merge-gate.js +5 -2
- package/dist/policy-packs/builtin/post-merge-gate.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution/index.d.ts +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution/index.js +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution/index.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.d.ts +127 -16
- package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js +192 -45
- package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution.d.ts +3 -2
- package/dist/policy-packs/builtin/understanding-before-execution.js +4 -1
- package/dist/policy-packs/builtin/understanding-before-execution.js.map +1 -1
- package/dist/runtime/deletion-target-resolve.js +24 -1
- package/dist/runtime/deletion-target-resolve.js.map +1 -1
- package/dist/runtime/destructive-shell-floor.js +15 -1
- package/dist/runtime/destructive-shell-floor.js.map +1 -1
- package/dist/runtime/shell-word.d.ts +26 -0
- package/dist/runtime/shell-word.js +94 -7
- package/dist/runtime/shell-word.js.map +1 -1
- package/package.json +1 -1
|
@@ -65,11 +65,6 @@ const HOOK_ID_LINE_RE = /^# harness hook: (.+) \(budget_ms=\d+\)$/;
|
|
|
65
65
|
// the two apart by the `# harness hook:` comment above the header instead
|
|
66
66
|
// (see `scanOwnedContentEnd`).
|
|
67
67
|
const HOOK_ARRAY_HEADER_RE = /^\[\[hooks\./;
|
|
68
|
-
/** True when a run of comment/blank lines holds a line the generator writes
|
|
69
|
-
* above every hook table (`# harness hook: <id> (budget_ms=N)`). */
|
|
70
|
-
function runHoldsHarnessHookComment(run) {
|
|
71
|
-
return run.split("\n").some((line) => line.trim().startsWith(HARNESS_HOOK_COMMENT_PREFIX));
|
|
72
|
-
}
|
|
73
68
|
export function defaultCodexConfigPath(homeDir) {
|
|
74
69
|
return path.join(homeDir ?? os.homedir(), ".codex", "config.toml");
|
|
75
70
|
}
|
|
@@ -189,35 +184,120 @@ function nextTripleDelim(rawLine, delimIn) {
|
|
|
189
184
|
return delim;
|
|
190
185
|
}
|
|
191
186
|
/**
|
|
192
|
-
* The
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
187
|
+
* The one rule every managed-block marker is located by (task 36d962d2): a
|
|
188
|
+
* line is a marker line only when its trimmed text is the marker (`whole`)
|
|
189
|
+
* or starts with it (`prefix`) AND it sits at the top level of the document
|
|
190
|
+
* (`createTopLevelProbe`). Marker text anywhere else is operator content:
|
|
191
|
+
* inside a single-line or multi-line string (basic or literal), after a
|
|
192
|
+
* content line as a trailing comment, inside an inline table, or on a line
|
|
193
|
+
* inside a multi-line array (an element, or a comment line between
|
|
194
|
+
* elements). Treating such text as a marker made the install replace,
|
|
195
|
+
* delete, or splice a fresh block into the operator's value. Every lookup
|
|
196
|
+
* of BEGIN, END, the source prefix, the generated header and the
|
|
197
|
+
* `# harness hook:` comment goes through this probe or `findMarkerLine` (or
|
|
198
|
+
* `findMarkerAboveHookTable`, which calls the probe for its one candidate),
|
|
199
|
+
* and so does the `[[hooks.` header line that lets a generated header open a
|
|
200
|
+
* legacy block (`findManagedRange`), searched only up to the end of the range
|
|
201
|
+
* the scan owns.
|
|
202
|
+
* Two marker searches stay outside it on purpose: the split-block refusal's
|
|
203
|
+
* candidate list (`assertNoSplitBlock`), which can only refuse, never
|
|
204
|
+
* install, and the trailing comment run of `scanOwnedContentEnd`, whose
|
|
205
|
+
* lines are whole top-level comment lines by construction. That run (blank
|
|
206
|
+
* and comment lines ending the file after a block with no END marker) is
|
|
207
|
+
* consumed as stale harness output when the source prefix, the generated
|
|
208
|
+
* header or the `# harness hook:` text appears anywhere in it, so an
|
|
209
|
+
* operator comment there that mentions such text mid-line is removed with
|
|
210
|
+
* the block.
|
|
211
|
+
*
|
|
212
|
+
* The returned probe answers for line starts queried in non-decreasing
|
|
213
|
+
* order (the top-level probe's contract, which a line that fails the text
|
|
214
|
+
* test never touches).
|
|
207
215
|
*/
|
|
208
|
-
function
|
|
216
|
+
function createMarkerLineProbe(text) {
|
|
217
|
+
const topLevel = createTopLevelProbe(text);
|
|
218
|
+
return (lineStart, marker, shape) => {
|
|
219
|
+
const trimmed = text.slice(lineStart, lineEndAfter(text, lineStart)).trim();
|
|
220
|
+
const textMatches = shape === "whole" ? trimmed === marker : trimmed.startsWith(marker);
|
|
221
|
+
return textMatches && topLevel(lineStart);
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/** The start of the first marker line (`createMarkerLineProbe`) at or after
|
|
225
|
+
* the line start `from` and before `to`, or -1. Candidates are visited in
|
|
226
|
+
* order, one line each, so the probe's non-decreasing-offset contract holds.
|
|
227
|
+
* Every candidate that fails the text test is free, but one that passes it
|
|
228
|
+
* costs a parse of the document before it (`createTopLevelProbe`), so a caller
|
|
229
|
+
* that only needs a marker inside a known range passes that range's end as
|
|
230
|
+
* `to`: the search stops there instead of probing every candidate past it. */
|
|
231
|
+
function findMarkerLine(text, marker, shape, from = 0, to = text.length) {
|
|
232
|
+
const isMarkerLine = createMarkerLineProbe(text);
|
|
209
233
|
let pos = from;
|
|
234
|
+
for (;;) {
|
|
235
|
+
const idx = text.indexOf(marker, pos);
|
|
236
|
+
if (idx === -1)
|
|
237
|
+
return -1;
|
|
238
|
+
const lineStart = lineStartAt(text, idx);
|
|
239
|
+
if (lineStart >= to)
|
|
240
|
+
return -1;
|
|
241
|
+
if (isMarkerLine(lineStart, marker, shape))
|
|
242
|
+
return lineStart;
|
|
243
|
+
pos = lineEndAfter(text, lineStart);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* The marker line (`createMarkerLineProbe`, prefix shape) that sits in the run
|
|
248
|
+
* of blank and comment lines directly above a hook table header, searched from
|
|
249
|
+
* the line start `from` (a line at which nothing is open) to the end of the
|
|
250
|
+
* document, with the header's offset; null when there is none. `isHookTable`
|
|
251
|
+
* says which header lines count as a hook table. This is how the two orphan
|
|
252
|
+
* shapes are found: a generated-header line above a hook table past a foreign
|
|
253
|
+
* table, and a `# harness hook:` comment line above a hook table with no block
|
|
254
|
+
* marker around it. The scan is linear and parses nothing for a line that is
|
|
255
|
+
* not such a pair: a line inside a multi-line string is skipped
|
|
256
|
+
* (`nextTripleDelim`), a content line ends the run, and only a marker-text
|
|
257
|
+
* line directly above a header reaches the probe, once, which confirms the
|
|
258
|
+
* line is at the top level. A comment run directly above a hook table header
|
|
259
|
+
* is at the top level in any config that parses (a header line is never inside
|
|
260
|
+
* an array), so the probe only turns away a config that does not parse, which
|
|
261
|
+
* the parse checks refuse anyway.
|
|
262
|
+
*/
|
|
263
|
+
function findMarkerAboveHookTable(text, marker, from, isHookTable) {
|
|
264
|
+
const isMarkerLine = createMarkerLineProbe(text);
|
|
210
265
|
let delim = null;
|
|
266
|
+
let runMarker = null;
|
|
267
|
+
let pos = from;
|
|
211
268
|
while (pos < text.length) {
|
|
212
269
|
const lineEnd = lineEndAfter(text, pos);
|
|
213
270
|
const rawLine = text.slice(pos, lineEnd);
|
|
214
271
|
const startedInsideString = delim !== null;
|
|
215
272
|
delim = nextTripleDelim(rawLine, delim);
|
|
216
|
-
if (
|
|
217
|
-
|
|
273
|
+
if (startedInsideString) {
|
|
274
|
+
runMarker = null;
|
|
275
|
+
pos = lineEnd;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const trimmed = rawLine.trim();
|
|
279
|
+
if (trimmed === "" || trimmed.startsWith("#")) {
|
|
280
|
+
if (runMarker === null && trimmed.startsWith(marker))
|
|
281
|
+
runMarker = pos;
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
if (runMarker !== null && isHookTable(trimmed) && isMarkerLine(runMarker, marker, "prefix")) {
|
|
285
|
+
return { marker: runMarker, header: pos };
|
|
286
|
+
}
|
|
287
|
+
runMarker = null;
|
|
288
|
+
}
|
|
218
289
|
pos = lineEnd;
|
|
219
290
|
}
|
|
220
|
-
return
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
/** True when a line in `[from, to)` is a marker line for the prefix
|
|
294
|
+
* `marker`. */
|
|
295
|
+
function rangeHoldsMarkerLine(text, from, to, marker, isMarkerLine) {
|
|
296
|
+
for (let pos = from; pos < to; pos = lineEndAfter(text, pos)) {
|
|
297
|
+
if (isMarkerLine(pos, marker, "prefix"))
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
return false;
|
|
221
301
|
}
|
|
222
302
|
/** Answers, for a line start in `text`, whether that line sits at the top
|
|
223
303
|
* level of the document, that is, not inside a multi-line array, a multi-line
|
|
@@ -398,7 +478,15 @@ function scanOwnedContentEnd(text, start) {
|
|
|
398
478
|
let commentRunStart = null;
|
|
399
479
|
let sawOwnedContent = false;
|
|
400
480
|
let sawMarkedHookTable = false;
|
|
481
|
+
let firstContentLine = null;
|
|
482
|
+
let firstHookHeader = null;
|
|
483
|
+
// A key line before the first hook table header of the range, with that
|
|
484
|
+
// header, when both exist: see `OwnedContentScan.operatorLineBeforeHookTable`.
|
|
485
|
+
const keyBeforeHookTable = () => firstContentLine !== null && firstHookHeader !== null
|
|
486
|
+
? { operatorLineBeforeHookTable: { line: firstContentLine, header: firstHookHeader } }
|
|
487
|
+
: {};
|
|
401
488
|
const classify = createLineClassifier(text);
|
|
489
|
+
const isMarkerLine = createMarkerLineProbe(text);
|
|
402
490
|
while (pos < text.length) {
|
|
403
491
|
const lineEnd = lineEndAfter(text, pos);
|
|
404
492
|
const rawLine = text.slice(pos, lineEnd);
|
|
@@ -409,8 +497,8 @@ function scanOwnedContentEnd(text, start) {
|
|
|
409
497
|
pos = lineEnd;
|
|
410
498
|
continue;
|
|
411
499
|
}
|
|
412
|
-
if (
|
|
413
|
-
return { end: lineEnd, sawEndMarker: true };
|
|
500
|
+
if (isMarkerLine(pos, CODEX_MANAGED_END, "whole")) {
|
|
501
|
+
return { end: lineEnd, sawEndMarker: true, ...keyBeforeHookTable() };
|
|
414
502
|
}
|
|
415
503
|
if (kind === "comment") {
|
|
416
504
|
if (commentRunStart === null)
|
|
@@ -419,11 +507,12 @@ function scanOwnedContentEnd(text, start) {
|
|
|
419
507
|
continue;
|
|
420
508
|
}
|
|
421
509
|
if (kind === "foreignHeader") {
|
|
422
|
-
return { end: commentRunStart ?? pos, sawEndMarker: false };
|
|
510
|
+
return { end: commentRunStart ?? pos, sawEndMarker: false, ...keyBeforeHookTable() };
|
|
423
511
|
}
|
|
424
512
|
if (kind === "hookHeader") {
|
|
513
|
+
firstHookHeader ??= pos;
|
|
425
514
|
const marked = commentRunStart !== null &&
|
|
426
|
-
|
|
515
|
+
rangeHoldsMarkerLine(text, commentRunStart, pos, HARNESS_HOOK_COMMENT_PREFIX, isMarkerLine);
|
|
427
516
|
if (marked) {
|
|
428
517
|
sawMarkedHookTable = true;
|
|
429
518
|
}
|
|
@@ -431,9 +520,17 @@ function scanOwnedContentEnd(text, start) {
|
|
|
431
520
|
// An operator-authored hook table (no `# harness hook:` comment
|
|
432
521
|
// above it) after the harness tables ends the owned region, like a
|
|
433
522
|
// foreign table, and keeps a comment attached to it (task 01053b27).
|
|
434
|
-
return {
|
|
523
|
+
return {
|
|
524
|
+
end: commentRunStart ?? pos,
|
|
525
|
+
sawEndMarker: false,
|
|
526
|
+
endedAtOperatorHookTable: true,
|
|
527
|
+
...keyBeforeHookTable(),
|
|
528
|
+
};
|
|
435
529
|
}
|
|
436
530
|
}
|
|
531
|
+
else if (firstHookHeader === null) {
|
|
532
|
+
firstContentLine ??= pos;
|
|
533
|
+
}
|
|
437
534
|
commentRunStart = null;
|
|
438
535
|
sawOwnedContent = true;
|
|
439
536
|
pos = lineEnd;
|
|
@@ -449,14 +546,46 @@ function scanOwnedContentEnd(text, start) {
|
|
|
449
546
|
if (commentRunStart !== null && sawOwnedContent) {
|
|
450
547
|
const run = text.slice(commentRunStart, pos);
|
|
451
548
|
const hasComment = /(^|\n)[ \t]*#/.test(run);
|
|
549
|
+
// Matched as substrings on purpose, outside `createMarkerLineProbe`:
|
|
550
|
+
// every line of this run is a whole comment or blank line at the top
|
|
551
|
+
// level (a string line resets the run, and at the end of a document that
|
|
552
|
+
// parses nothing is still open), so marker text here is never inside a
|
|
553
|
+
// value, and a comment that mentions a marker is stale harness output.
|
|
452
554
|
const harnessAuthored = run.includes(HARNESS_HOOK_COMMENT_PREFIX) ||
|
|
453
555
|
run.includes(CODEX_MANAGED_SOURCE_PREFIX) ||
|
|
454
556
|
run.includes(GENERATED_HEADER);
|
|
455
557
|
if (hasComment && !harnessAuthored) {
|
|
456
|
-
return { end: commentRunStart, sawEndMarker: false };
|
|
558
|
+
return { end: commentRunStart, sawEndMarker: false, ...keyBeforeHookTable() };
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return { end: pos, sawEndMarker: false, ...keyBeforeHookTable() };
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* True when a harness-commented hook table (a `# harness hook:` comment line
|
|
565
|
+
* followed, later in the zone, by a `[[hooks.` header or a hook event array
|
|
566
|
+
* header spelled without that prefix, `[[ hooks.SessionStart ]]`) lies after the named
|
|
567
|
+
* table's header line, before `to`. The comment is found as a marker line
|
|
568
|
+
* (`createMarkerLineProbe`), so a value that merely contains the marker text
|
|
569
|
+
* does not count. Used only to pick the fix guidance of a refusal (task
|
|
570
|
+
* 36d962d2).
|
|
571
|
+
*/
|
|
572
|
+
function harnessHookTableFollows(text, header, to) {
|
|
573
|
+
if (header === null)
|
|
574
|
+
return false;
|
|
575
|
+
let pos = lineEndAfter(text, header.start);
|
|
576
|
+
let sawComment = false;
|
|
577
|
+
const isMarkerLine = createMarkerLineProbe(text);
|
|
578
|
+
while (pos < to) {
|
|
579
|
+
const lineEnd = Math.min(lineEndAfter(text, pos), to);
|
|
580
|
+
const trimmed = text.slice(pos, lineEnd).trim();
|
|
581
|
+
if (isMarkerLine(pos, HARNESS_HOOK_COMMENT_PREFIX, "prefix"))
|
|
582
|
+
sawComment = true;
|
|
583
|
+
else if (sawComment && (HOOK_ARRAY_HEADER_RE.test(trimmed) || isHookEventArrayHeader(trimmed))) {
|
|
584
|
+
return true;
|
|
457
585
|
}
|
|
586
|
+
pos = lineEnd;
|
|
458
587
|
}
|
|
459
|
-
return
|
|
588
|
+
return false;
|
|
460
589
|
}
|
|
461
590
|
/**
|
|
462
591
|
* Refuses loudly (mirror case of the stray-END bug) when the zone between
|
|
@@ -485,20 +614,37 @@ function scanOwnedContentEnd(text, start) {
|
|
|
485
614
|
* point at that marker; when there is no END marker anywhere in the
|
|
486
615
|
* document (`to` is `text.length`), telling the operator to move something
|
|
487
616
|
* below a marker that does not exist would be nonsensical, so a different
|
|
488
|
-
* fix (
|
|
489
|
-
*
|
|
490
|
-
* table
|
|
491
|
-
*
|
|
492
|
-
*
|
|
617
|
+
* fix is offered instead (task 6a037359): adding one right after the last
|
|
618
|
+
* harness hook table, unless a harness-commented hook table follows the named
|
|
619
|
+
* table (`harnessHookTableFollows`), since an END placed before the named table
|
|
620
|
+
* would leave that later table running next to the fresh block, and then only
|
|
621
|
+
* moving the named table below the last harness hook table is offered (task
|
|
622
|
+
* 36d962d2). With no such table after it the named table already sits below
|
|
623
|
+
* the last harness hook table, so that move is never offered next to the END
|
|
624
|
+
* fix: it would change nothing and bring the same refusal back.
|
|
625
|
+
* When the named table is a hook event table spelled without the `[[hooks.`
|
|
626
|
+
* prefix, the guidance offers deleting it if harness wrote it, since the scan
|
|
627
|
+
* reads such a table as foreign with or without a comment line above it
|
|
628
|
+
* (task e8f4fc03). The legacy paths call
|
|
493
629
|
* this too, so the substring matching above applies there as well: a value
|
|
494
|
-
* holding marker text refuses.
|
|
630
|
+
* holding marker text refuses. This candidate list and the trailing comment
|
|
631
|
+
* run of `scanOwnedContentEnd` are the two marker searches that do not go
|
|
632
|
+
* through `createMarkerLineProbe`, on purpose. This one can only refuse,
|
|
633
|
+
* never install, so matching marker text in a value costs a refusal, never
|
|
634
|
+
* operator content. The comment run's lines are whole top-level comment
|
|
635
|
+
* lines, so marker text there is never inside a value, but that run (blank
|
|
636
|
+
* and comment lines ending the file after a block with no END marker) is
|
|
637
|
+
* consumed when the source prefix, the generated header or the
|
|
638
|
+
* `# harness hook:` text appears anywhere in it, so an operator comment
|
|
639
|
+
* there that mentions such text mid-line is removed with the block.
|
|
640
|
+
* `blockOpening` names the line the block
|
|
495
641
|
* starts at in the delete-the-lines fix: the BEGIN marker by default; a
|
|
496
642
|
* legacy config (no BEGIN marker) whose END marker survived passes its
|
|
497
643
|
* source-prefix line or generated header instead, since a BEGIN marker it
|
|
498
644
|
* does not have is no line to delete from (task 01053b27). For the generated
|
|
499
645
|
* header the fix deletes that line and the END marker line too: a generated
|
|
500
646
|
* header left alone above END no longer marks a harness block
|
|
501
|
-
* (`findManagedRange` needs a `[[hooks.` line after it), so the install
|
|
647
|
+
* (`findManagedRange` needs a top-level `[[hooks.` header line after it), so the install
|
|
502
648
|
* would add a fresh block and leave that pair behind as an orphan. Exported for
|
|
503
649
|
* direct tests only: the scan
|
|
504
650
|
* that feeds it never hands it a line that is not a real header, so its own
|
|
@@ -550,11 +696,20 @@ export function assertNoSplitBlock(text, from, to, configPath, hasEndMarker, blo
|
|
|
550
696
|
: `delete the lines between ${blockOpening} and '${CODEX_MANAGED_END}' so only ` +
|
|
551
697
|
"harness-owned tables remain between them",
|
|
552
698
|
]
|
|
553
|
-
:
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
699
|
+
: harnessHookTableFollows(text, foreignHeaderLine, zoneEnd)
|
|
700
|
+
? // An END marker placed before the named table would leave the
|
|
701
|
+
// harness-commented hook table after it running next to the fresh
|
|
702
|
+
// block, so the only fix offered is moving the named table past it
|
|
703
|
+
// (task 36d962d2).
|
|
704
|
+
[`move ${firstForeignHeader} below the last harness hook table`]
|
|
705
|
+
: // No harness-commented hook table follows the named table, so it
|
|
706
|
+
// already sits below the last harness hook table and moving it there
|
|
707
|
+
// changes nothing (the same refusal would come back): only the END
|
|
708
|
+
// marker is offered.
|
|
709
|
+
[
|
|
710
|
+
`add a '${CODEX_MANAGED_END}' marker line right after the last harness hook table ` +
|
|
711
|
+
`(before ${firstForeignHeader})`,
|
|
712
|
+
];
|
|
558
713
|
// An uncommented hook table in the middle may be a harness table whose
|
|
559
714
|
// comment line was deleted, so moving it (the operator-table fix) is only
|
|
560
715
|
// right for an operator-owned one; name the restore option first (task
|
|
@@ -690,10 +845,16 @@ function collectForeignSectionHeaders(text, from, to, kept = []) {
|
|
|
690
845
|
return count > 1 ? `[${root}.*] (${count} tables)` : header;
|
|
691
846
|
});
|
|
692
847
|
}
|
|
693
|
-
|
|
848
|
+
/** The hook ids named by the `# harness hook:` marker lines
|
|
849
|
+
* (`createMarkerLineProbe`) in `[from, to)` of `text`, so a value holding
|
|
850
|
+
* that text is never reported as a removed hook. */
|
|
851
|
+
function extractHookIds(text, from = 0, to = text.length) {
|
|
694
852
|
const ids = new Set();
|
|
695
|
-
|
|
696
|
-
|
|
853
|
+
const isMarkerLine = createMarkerLineProbe(text);
|
|
854
|
+
for (let pos = from; pos < to; pos = lineEndAfter(text, pos)) {
|
|
855
|
+
if (!isMarkerLine(pos, HARNESS_HOOK_COMMENT_PREFIX, "prefix"))
|
|
856
|
+
continue;
|
|
857
|
+
const m = HOOK_ID_LINE_RE.exec(text.slice(pos, lineEndAfter(text, pos)).trim());
|
|
697
858
|
if (m?.[1] !== undefined)
|
|
698
859
|
ids.add(m[1]);
|
|
699
860
|
}
|
|
@@ -765,8 +926,8 @@ function startPastBom(text, start) {
|
|
|
765
926
|
* `blockOpening` is the line the split-block refusal names as the start of
|
|
766
927
|
* the block (`assertNoSplitBlock`; the BEGIN marker when omitted).
|
|
767
928
|
*/
|
|
768
|
-
function rangeBeforeStrayEnd(text, start, end, configPath, blockOpening) {
|
|
769
|
-
const strayIdx =
|
|
929
|
+
function rangeBeforeStrayEnd(text, start, end, configPath, blockOpening, zoneFrom = end) {
|
|
930
|
+
const strayIdx = findMarkerLine(text, CODEX_MANAGED_END, "whole", end);
|
|
770
931
|
if (strayIdx === -1) {
|
|
771
932
|
assertNoSplitBlock(text, end, text.length, configPath, false);
|
|
772
933
|
const keptHookTables = collectKeptHookTables(text, end, text.length).tables;
|
|
@@ -775,7 +936,7 @@ function rangeBeforeStrayEnd(text, start, end, configPath, blockOpening) {
|
|
|
775
936
|
}
|
|
776
937
|
const strayLineStart = lineStartAt(text, strayIdx);
|
|
777
938
|
const strayLineEnd = lineEndAfter(text, strayIdx);
|
|
778
|
-
assertNoSplitBlock(text,
|
|
939
|
+
assertNoSplitBlock(text, zoneFrom, strayLineStart, configPath, true, blockOpening);
|
|
779
940
|
const keptHookTables = collectKeptHookTables(text, end, strayLineStart).tables;
|
|
780
941
|
const foreignSectionsPreserved = collectForeignSectionHeaders(text, end, strayLineStart, keptHookTables);
|
|
781
942
|
return {
|
|
@@ -786,6 +947,25 @@ function rangeBeforeStrayEnd(text, start, end, configPath, blockOpening) {
|
|
|
786
947
|
keptHookTables,
|
|
787
948
|
};
|
|
788
949
|
}
|
|
950
|
+
/** Refuses operator key/value content before the first hook table on every
|
|
951
|
+
* locator path. The ownership scan supplies offsets; diagnostics print no values. */
|
|
952
|
+
function assertNoOperatorLineBeforeHookTable(text, start, scan, configPath, blockOpening) {
|
|
953
|
+
if (scan.operatorLineBeforeHookTable !== undefined) {
|
|
954
|
+
const { line, header } = scan.operatorLineBeforeHookTable;
|
|
955
|
+
// Names line numbers only, never the line's own text: it is a key or a
|
|
956
|
+
// value, which can hold a token, and the refusal is printed to stderr and
|
|
957
|
+
// `--json`.
|
|
958
|
+
throw new CodexInstallRefusalError(`Codex config ${configPath} has a key or value line (line ${lineNumberAt(text, line)}) between ` +
|
|
959
|
+
`${blockOpening} (line ${lineNumberAt(text, start)}) and the first hook table below it (line ` +
|
|
960
|
+
`${lineNumberAt(text, header)}). A block harness wrote has only comment lines there, so that ` +
|
|
961
|
+
"line is yours, a key of whatever table is open where the opening line sits (an operator " +
|
|
962
|
+
"hook table, say), and the install, which replaces everything from the opening line " +
|
|
963
|
+
"through the harness hook tables, would delete it; refusing to guess. Move that line (with " +
|
|
964
|
+
"any value lines it continues) above the opening line, so it stays in the table it " +
|
|
965
|
+
"belongs to, then re-run " +
|
|
966
|
+
"`harness apply --runtime codex --install`. The file is untouched.", configPath);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
789
969
|
/**
|
|
790
970
|
* The range of a legacy (no BEGIN marker) config: the source-prefix or
|
|
791
971
|
* generated-header paths. When the scan ended at an operator hook table,
|
|
@@ -799,26 +979,37 @@ function rangeBeforeStrayEnd(text, start, end, configPath, blockOpening) {
|
|
|
799
979
|
* kept as if it were operator content (the fresh block would repeat it)
|
|
800
980
|
* when it sits before that END, or, after kept tables, anywhere later when
|
|
801
981
|
* no END follows (task e8f4fc03 extends the END case to a foreign table).
|
|
802
|
-
* Content below that END is
|
|
803
|
-
* harness-commented table
|
|
982
|
+
* Content below that END is kept, as on the BEGIN path; the global orphan
|
|
983
|
+
* check refuses a harness-commented hook table outside the owned range. The split-block
|
|
804
984
|
* refusal names `blockOpening` (the source-prefix line or the generated
|
|
805
985
|
* header) as the start of the block, since there is no BEGIN marker (task
|
|
806
|
-
* 01053b27)
|
|
807
|
-
*
|
|
808
|
-
*
|
|
986
|
+
* 01053b27); the split-block zone starts after that opening line, so a block
|
|
987
|
+
* with no hook table in it (the source prefix directly above an operator
|
|
988
|
+
* table) is not refused for its own opening line (task 36d962d2). The opening
|
|
989
|
+
* line is a marker line (`createMarkerLineProbe`), so marker text inside a
|
|
990
|
+
* value or after a content line opens no block.
|
|
991
|
+
* A scan that ended at a foreign table with no END marker anywhere after it
|
|
992
|
+
* keeps the legacy behaviour (no split-block check), except that hook event tables directly after the block are collected, named and
|
|
809
993
|
* checked (`collectKeptHookTables`), and a harness marker in their run
|
|
810
994
|
* refuses through the split-block check. The END marker is looked up as a
|
|
811
|
-
*
|
|
812
|
-
*
|
|
995
|
+
* marker line too, so END text in a foreign table's value does not route a
|
|
996
|
+
* config here. Like the BEGIN path,
|
|
813
997
|
* the split-block check matches its markers as substrings, so a value in the
|
|
814
998
|
* zone that holds marker text refuses too (see `assertNoSplitBlock`).
|
|
815
999
|
*/
|
|
816
1000
|
function legacyManagedRange(text, start, scan, configPath, blockOpening) {
|
|
1001
|
+
assertNoOperatorLineBeforeHookTable(text, start, scan, configPath, blockOpening);
|
|
817
1002
|
if (scan.sawEndMarker) {
|
|
818
1003
|
return { start, end: scan.end, foreignSectionsPreserved: [], keptHookTables: [] };
|
|
819
1004
|
}
|
|
820
|
-
|
|
821
|
-
|
|
1005
|
+
// The split-block zone starts after the block's own opening line: when the
|
|
1006
|
+
// scan owned nothing (the generated header or a foreign table directly
|
|
1007
|
+
// follows the source prefix), `scan.end` is that opening line itself, which
|
|
1008
|
+
// is a split-block marker and would refuse a block with nothing in it
|
|
1009
|
+
// (task 36d962d2).
|
|
1010
|
+
const zoneFrom = Math.max(scan.end, lineEndAfter(text, start));
|
|
1011
|
+
if (scan.endedAtOperatorHookTable || findMarkerLine(text, CODEX_MANAGED_END, "whole", scan.end) !== -1) {
|
|
1012
|
+
return rangeBeforeStrayEnd(text, start, scan.end, configPath, blockOpening, zoneFrom);
|
|
822
1013
|
}
|
|
823
1014
|
const run = collectKeptHookTables(text, scan.end, text.length);
|
|
824
1015
|
// The scan ended at a hook table spelled without the `[[hooks.` prefix: a
|
|
@@ -829,21 +1020,23 @@ function legacyManagedRange(text, start, scan, configPath, blockOpening) {
|
|
|
829
1020
|
// as one without (task e8f4fc03). The run stops at the first non-hook table:
|
|
830
1021
|
// past it the legacy reading below stays unchanged.
|
|
831
1022
|
if (run.tables.length > 0)
|
|
832
|
-
assertNoSplitBlock(text,
|
|
1023
|
+
assertNoSplitBlock(text, zoneFrom, run.end, configPath, false);
|
|
833
1024
|
const keptHookTables = run.tables;
|
|
834
1025
|
const foreignSectionsPreserved = collectForeignSectionHeaders(text, scan.end, text.length, keptHookTables);
|
|
835
1026
|
return { start, end: scan.end, foreignSectionsPreserved, keptHookTables };
|
|
836
1027
|
}
|
|
837
1028
|
function findManagedRange(text, configPath) {
|
|
838
|
-
|
|
1029
|
+
// BEGIN is found as a marker line (`createMarkerLineProbe`): BEGIN text in
|
|
1030
|
+
// an operator value (a string, a trailing comment) is neither the block nor
|
|
1031
|
+
// a second marker, so the install neither deletes operator content after
|
|
1032
|
+
// it nor splices the fresh block into the value (task 36d962d2).
|
|
1033
|
+
const begin = findMarkerLine(text, CODEX_MANAGED_BEGIN, "prefix");
|
|
839
1034
|
if (begin !== -1) {
|
|
840
|
-
const secondBegin = text
|
|
1035
|
+
const secondBegin = findMarkerLine(text, CODEX_MANAGED_BEGIN, "prefix", lineEndAfter(text, begin));
|
|
841
1036
|
if (secondBegin !== -1) {
|
|
842
1037
|
// Names the second marker's line number only, never that line's own
|
|
843
|
-
// text
|
|
844
|
-
|
|
845
|
-
// the marker text, and this refusal is printed to stderr and `--json`.
|
|
846
|
-
const secondBeginLineNumber = lineNumberAt(text, lineStartAt(text, secondBegin));
|
|
1038
|
+
// text, since this refusal is printed to stderr and `--json`.
|
|
1039
|
+
const secondBeginLineNumber = lineNumberAt(text, secondBegin);
|
|
847
1040
|
throw new CodexInstallRefusalError(`Codex config ${configPath} has more than one '${CODEX_MANAGED_BEGIN}' marker ` +
|
|
848
1041
|
`(a second one at line ${secondBeginLineNumber}); refusing to ` +
|
|
849
1042
|
"guess which one is current. Delete the entire unwanted BEGIN/END pair -- including " +
|
|
@@ -854,42 +1047,114 @@ function findManagedRange(text, configPath) {
|
|
|
854
1047
|
"but that discards any Codex state (hook trust entries, marketplaces, plugins) " +
|
|
855
1048
|
"written to the file since that backup.", configPath);
|
|
856
1049
|
}
|
|
857
|
-
let start =
|
|
1050
|
+
let start = begin;
|
|
858
1051
|
const prev = previousLine(text, start);
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1052
|
+
// Only a marker line is the block's source prefix: the closing line of a
|
|
1053
|
+
// multi-line string that ends with the prefix text is the operator's
|
|
1054
|
+
// value, not part of the block (task 36d962d2). Trimming also drops a BOM
|
|
1055
|
+
// sitting before the source-prefix comment on line 1 (task b34ed105).
|
|
1056
|
+
if (prev && createMarkerLineProbe(text)(prev.start, CODEX_MANAGED_SOURCE_PREFIX, "prefix")) {
|
|
1057
|
+
start = prev.start;
|
|
865
1058
|
}
|
|
866
1059
|
start = startPastBom(text, start);
|
|
867
1060
|
const contentStart = lineEndAfter(text, begin);
|
|
868
1061
|
const scan = scanOwnedContentEnd(text, contentStart);
|
|
1062
|
+
assertNoOperatorLineBeforeHookTable(text, begin, scan, configPath, `the '${CODEX_MANAGED_BEGIN}' line`);
|
|
869
1063
|
const end = scan.end;
|
|
870
1064
|
if (scan.sawEndMarker) {
|
|
871
1065
|
return { start, end, foreignSectionsPreserved: [], keptHookTables: [] };
|
|
872
1066
|
}
|
|
873
1067
|
return rangeBeforeStrayEnd(text, start, end, configPath);
|
|
874
1068
|
}
|
|
875
|
-
const source = text
|
|
1069
|
+
const source = findMarkerLine(text, CODEX_MANAGED_SOURCE_PREFIX, "prefix");
|
|
876
1070
|
if (source !== -1) {
|
|
877
|
-
const start = startPastBom(text,
|
|
1071
|
+
const start = startPastBom(text, source);
|
|
878
1072
|
return legacyManagedRange(text, start, scanOwnedContentEnd(text, start), configPath, `the '${CODEX_MANAGED_SOURCE_PREFIX}' source-prefix line`);
|
|
879
1073
|
}
|
|
880
|
-
const generated = text
|
|
1074
|
+
const generated = findMarkerLine(text, GENERATED_HEADER, "prefix");
|
|
881
1075
|
if (generated !== -1) {
|
|
882
|
-
const start = startPastBom(text,
|
|
1076
|
+
const start = startPastBom(text, generated);
|
|
883
1077
|
const scan = scanOwnedContentEnd(text, start);
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1078
|
+
// The generated header opens a legacy block only when a hook table header
|
|
1079
|
+
// line (`[[hooks.`, the older `[[hooks.pre_tool_use]]` included) follows
|
|
1080
|
+
// it inside the scanned range, found by the same top-level line rule as
|
|
1081
|
+
// the markers: `[[hooks.` text in a value, a multi-line string line or a
|
|
1082
|
+
// comment under an operator table is operator content, and taking it for
|
|
1083
|
+
// a header made the install replace the operator's keys after the header
|
|
1084
|
+
// line with the fresh block (task 36d962d2).
|
|
1085
|
+
// The search stops at the end of the scanned range: a candidate past it
|
|
1086
|
+
// cannot open the block, and each one the text test lets through costs a
|
|
1087
|
+
// parse of the document before it.
|
|
1088
|
+
const hookHeader = findMarkerLine(text, "[[hooks.", "prefix", start, scan.end);
|
|
1089
|
+
if (hookHeader !== -1) {
|
|
888
1090
|
return legacyManagedRange(text, start, scan, configPath, GENERATED_HEADER_OPENING);
|
|
889
1091
|
}
|
|
1092
|
+
assertNoLaterGeneratedHeaderBlock(text, generated, scan.end, configPath);
|
|
890
1093
|
}
|
|
1094
|
+
assertNoOrphanHarnessHookTable(text, configPath);
|
|
891
1095
|
return null;
|
|
892
1096
|
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Refuses when a generated-header line outside the range being replaced sits
|
|
1099
|
+
* directly above a hook table, either after a stale first header or after a
|
|
1100
|
+
* genuine block: that is a genuine legacy block, and the install
|
|
1101
|
+
* reads only the first generated-header line, so it would add the fresh block
|
|
1102
|
+
* and leave the genuine one running next to it. Which of the two lines to
|
|
1103
|
+
* trust is not something the file says, so the install names both lines and
|
|
1104
|
+
* refuses. A later block whose first hook table is not directly under its
|
|
1105
|
+
* generated-header line (a key or value line in between) is not recognised
|
|
1106
|
+
* here, a documented boundary. Line numbers only, never a line's text.
|
|
1107
|
+
*/
|
|
1108
|
+
function assertNoLaterGeneratedHeaderBlock(text, firstHeader, from, configPath, firstBlockOwned = false) {
|
|
1109
|
+
const later = findMarkerAboveHookTable(text, GENERATED_HEADER, from, (t) => HOOK_ARRAY_HEADER_RE.test(t));
|
|
1110
|
+
if (later === null)
|
|
1111
|
+
return;
|
|
1112
|
+
const firstDescription = firstBlockOwned
|
|
1113
|
+
? `the one at line ${lineNumberAt(text, firstHeader)} belongs to the block being replaced`
|
|
1114
|
+
: `the one at line ${lineNumberAt(text, firstHeader)} has no hook table of its own (a table of yours follows it)`;
|
|
1115
|
+
const fix = firstBlockOwned
|
|
1116
|
+
? `Delete the unwanted later block, including its generated-header line and hook tables`
|
|
1117
|
+
: `Delete the line at line ${lineNumberAt(text, firstHeader)} (it is a comment, and no block hangs on it)`;
|
|
1118
|
+
throw new CodexInstallRefusalError(`Codex config ${configPath} has two '${GENERATED_HEADER}' lines and the install can replace ` +
|
|
1119
|
+
`only one block: ${firstDescription}, the one at line ${lineNumberAt(text, later.marker)} sits ` +
|
|
1120
|
+
`above the hook table at line ${lineNumberAt(text, later.header)}. Installing would add the ` +
|
|
1121
|
+
"fresh block and leave that hook table running next to it, so the install refuses to guess " +
|
|
1122
|
+
`which line is current. ${fix}, then re-run ` +
|
|
1123
|
+
"`harness apply --runtime codex --install`. The file is untouched.", configPath);
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Refuses outside the range being replaced (including the append path) when
|
|
1127
|
+
* a `# harness hook:` comment line sits directly above a hook table: that is a hook table an earlier install
|
|
1128
|
+
* wrote whose opening lines are gone, and the install would add the fresh block
|
|
1129
|
+
* and leave it running next to it (the hook would run twice, or a hook the
|
|
1130
|
+
* manifest retired would keep running). The comment line is found as a marker
|
|
1131
|
+
* line (`createMarkerLineProbe`), above a `[[hooks.` header or a hook event
|
|
1132
|
+
* header spelled another way (`isHookEventArrayHeader`). A table of the
|
|
1133
|
+
* operator's own that carries such a comment is refused too, since the two
|
|
1134
|
+
* cannot be told apart; the guidance offers rewording the comment. Line numbers
|
|
1135
|
+
* and the table's own header only, never a line's text.
|
|
1136
|
+
*/
|
|
1137
|
+
function assertNoOrphanHarnessHookTable(text, configPath, range = null) {
|
|
1138
|
+
let orphan = findMarkerAboveHookTable(text, HARNESS_HOOK_COMMENT_PREFIX, 0, (t) => HOOK_ARRAY_HEADER_RE.test(t) || isHookEventArrayHeader(t));
|
|
1139
|
+
// An owned pair is replaced together. Search the unowned tail from a known
|
|
1140
|
+
// top-level boundary; a pair before the range must still refuse.
|
|
1141
|
+
if (orphan !== null && range !== null && orphan.header >= range.start && orphan.header < range.end) {
|
|
1142
|
+
orphan = findMarkerAboveHookTable(text, HARNESS_HOOK_COMMENT_PREFIX, range.end, (t) => HOOK_ARRAY_HEADER_RE.test(t) || isHookEventArrayHeader(t));
|
|
1143
|
+
}
|
|
1144
|
+
if (orphan === null)
|
|
1145
|
+
return;
|
|
1146
|
+
const header = describeTableHeader(text.slice(orphan.header, lineEndAfter(text, orphan.header)));
|
|
1147
|
+
throw new CodexInstallRefusalError(`Codex config ${configPath} has a '${HARNESS_HOOK_COMMENT_PREFIX}<id> (budget_ms=<n>)' comment ` +
|
|
1148
|
+
`line (line ${lineNumberAt(text, orphan.marker)}) directly above the hook table ${header} (line ` +
|
|
1149
|
+
`${lineNumberAt(text, orphan.header)}), but no harness block marker (the '${CODEX_MANAGED_BEGIN}' ` +
|
|
1150
|
+
"line, the source-prefix line, or the generated-header line above a hook table) opens a block " +
|
|
1151
|
+
"around it. harness cannot tell that table from one an earlier install wrote, and the install " +
|
|
1152
|
+
"would add its fresh block next to it, so that hook would run twice (or a hook the manifest " +
|
|
1153
|
+
"retired would keep running). If harness wrote the table, delete it together with its comment " +
|
|
1154
|
+
"line. If it is your own table, delete or reword the comment line, since the install reads a " +
|
|
1155
|
+
`comment line that starts with '${HARNESS_HOOK_COMMENT_PREFIX}' as harness-owned. Then re-run ` +
|
|
1156
|
+
"`harness apply --runtime codex --install`. The file is untouched.", configPath);
|
|
1157
|
+
}
|
|
893
1158
|
function ensureTrailingNewline(s) {
|
|
894
1159
|
return s.endsWith("\n") ? s : `${s}\n`;
|
|
895
1160
|
}
|
|
@@ -1482,6 +1747,13 @@ export function planCodexConfigInstall(opts) {
|
|
|
1482
1747
|
}
|
|
1483
1748
|
const managedBlock = buildManagedBlock(opts.generatedContent, opts.generatedPath);
|
|
1484
1749
|
const range = withBackupLocation(linkPath, () => findManagedRange(currentContent, configPath));
|
|
1750
|
+
if (range !== null) {
|
|
1751
|
+
assertNoOrphanHarnessHookTable(currentContent, configPath, range);
|
|
1752
|
+
const firstGenerated = findMarkerLine(currentContent, GENERATED_HEADER, "prefix", range.start, range.end);
|
|
1753
|
+
if (firstGenerated !== -1) {
|
|
1754
|
+
assertNoLaterGeneratedHeaderBlock(currentContent, firstGenerated, range.end, configPath, true);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1485
1757
|
let nextContent;
|
|
1486
1758
|
let summary;
|
|
1487
1759
|
let removedHookIds = [];
|
|
@@ -1490,7 +1762,7 @@ export function planCodexConfigInstall(opts) {
|
|
|
1490
1762
|
let keptTableDescriptions = [];
|
|
1491
1763
|
let hasEndMarkerAfterKept = false;
|
|
1492
1764
|
if (range) {
|
|
1493
|
-
const oldHookIds = extractHookIds(currentContent
|
|
1765
|
+
const oldHookIds = extractHookIds(currentContent, range.start, range.end);
|
|
1494
1766
|
const newHookIds = extractHookIds(managedBlock);
|
|
1495
1767
|
removedHookIds = [...oldHookIds].filter((id) => !newHookIds.has(id));
|
|
1496
1768
|
keptHookTables = range.keptHookTables;
|