@lannguyensi/harness 0.59.2 → 0.61.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 +66 -0
- package/README.md +1 -1
- package/dist/cli/apply/apply.d.ts +20 -0
- package/dist/cli/apply/apply.js +13 -7
- package/dist/cli/apply/apply.js.map +1 -1
- package/dist/cli/apply/install-codex-config.d.ts +26 -4
- package/dist/cli/apply/install-codex-config.js +299 -120
- package/dist/cli/apply/install-codex-config.js.map +1 -1
- package/dist/cli/approve/understanding.d.ts +6 -6
- package/dist/cli/approve/understanding.js +62 -58
- package/dist/cli/approve/understanding.js.map +1 -1
- package/dist/cli/bounded-stdin.d.ts +27 -0
- package/dist/cli/bounded-stdin.js +82 -0
- package/dist/cli/bounded-stdin.js.map +1 -0
- package/dist/cli/doctor/format.js +22 -0
- package/dist/cli/doctor/format.js.map +1 -1
- package/dist/cli/doctor/index.js +60 -31
- package/dist/cli/doctor/index.js.map +1 -1
- package/dist/cli/doctor/settings-drift.js +12 -2
- package/dist/cli/doctor/settings-drift.js.map +1 -1
- package/dist/cli/doctor/types.d.ts +17 -0
- package/dist/cli/dry-run.js +7 -0
- package/dist/cli/dry-run.js.map +1 -1
- package/dist/cli/index.js +46 -144
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/pack/auto-approve-path.js +47 -13
- package/dist/cli/pack/auto-approve-path.js.map +1 -1
- package/dist/cli/pack/hook-bootstrap.d.ts +71 -5
- package/dist/cli/pack/hook-bootstrap.js +92 -18
- 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-post-tool-use.js +2 -2
- package/dist/cli/pack/hook-codex-post-tool-use.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 +52 -7
- package/dist/cli/pack/hook-codex-pre-tool-use.js.map +1 -1
- package/dist/cli/pack/hook-post-merge-gate-record.d.ts +7 -0
- package/dist/cli/pack/hook-post-merge-gate-record.js +39 -2
- package/dist/cli/pack/hook-post-merge-gate-record.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-post-tool-use.js +2 -2
- package/dist/cli/pack/hook-post-tool-use.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 +84 -18
- 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 +49 -9
- 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 +14 -13
- package/dist/cli/policy/intercept.js.map +1 -1
- package/dist/cli/record/index.d.ts +11 -2
- package/dist/cli/record/index.js +114 -25
- package/dist/cli/record/index.js.map +1 -1
- package/dist/cli/session-start/branch-check.d.ts +3 -29
- package/dist/cli/session-start/branch-check.js +10 -37
- package/dist/cli/session-start/branch-check.js.map +1 -1
- package/dist/cli/session-start/index.d.ts +3 -32
- package/dist/cli/session-start/index.js +19 -31
- package/dist/cli/session-start/index.js.map +1 -1
- package/dist/cli/session-start/shared-options.d.ts +103 -0
- package/dist/cli/session-start/shared-options.js +107 -0
- package/dist/cli/session-start/shared-options.js.map +1 -0
- package/dist/cli/session-start/stale-base-check.d.ts +3 -28
- package/dist/cli/session-start/stale-base-check.js +10 -37
- package/dist/cli/session-start/stale-base-check.js.map +1 -1
- package/dist/cli/session-start/toolchain-parity.d.ts +5 -21
- package/dist/cli/session-start/toolchain-parity.js +9 -38
- package/dist/cli/session-start/toolchain-parity.js.map +1 -1
- package/dist/cli/smoke/index.d.ts +1 -1
- package/dist/cli/smoke/index.js +8 -11
- package/dist/cli/smoke/index.js.map +1 -1
- package/dist/policy-packs/builtin/post-merge-gate-runtime.d.ts +11 -0
- package/dist/policy-packs/builtin/post-merge-gate-runtime.js +11 -0
- package/dist/policy-packs/builtin/post-merge-gate-runtime.js.map +1 -1
- 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/expiry-reason.d.ts +36 -0
- package/dist/policy-packs/builtin/understanding-before-execution/expiry-reason.js +73 -0
- package/dist/policy-packs/builtin/understanding-before-execution/expiry-reason.js.map +1 -0
- package/dist/policy-packs/builtin/understanding-before-execution/index.d.ts +3 -2
- package/dist/policy-packs/builtin/understanding-before-execution/index.js +5 -2
- package/dist/policy-packs/builtin/understanding-before-execution/index.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution/markers.js +1 -0
- package/dist/policy-packs/builtin/understanding-before-execution/markers.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.d.ts +253 -1
- package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js +429 -13
- package/dist/policy-packs/builtin/understanding-before-execution/persisted-reports.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.d.ts +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js +2 -2
- package/dist/policy-packs/builtin/understanding-before-execution/post-tool-use-boundary.js.map +1 -1
- package/dist/policy-packs/builtin/understanding-before-execution/task-markers.d.ts +36 -0
- package/dist/policy-packs/builtin/understanding-before-execution/task-markers.js +50 -0
- package/dist/policy-packs/builtin/understanding-before-execution/task-markers.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/bash-prefix-parse.js +319 -126
- package/dist/runtime/bash-prefix-parse.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/intercept.d.ts +38 -0
- package/dist/runtime/intercept.js +224 -5
- package/dist/runtime/intercept.js.map +1 -1
- package/dist/runtime/read-only-bash.js +11 -1
- package/dist/runtime/read-only-bash.js.map +1 -1
- package/dist/runtime/shell-word.d.ts +42 -1
- package/dist/runtime/shell-word.js +174 -6
- 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,65 @@ 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`, and
|
|
198
|
+
* so does the `[[hooks.` header line that lets a generated header open a
|
|
199
|
+
* legacy block (`findManagedRange`).
|
|
200
|
+
* Two marker searches stay outside it on purpose: the split-block refusal's
|
|
201
|
+
* candidate list (`assertNoSplitBlock`), which can only refuse, never
|
|
202
|
+
* install, and the trailing comment run of `scanOwnedContentEnd`, whose
|
|
203
|
+
* lines are whole top-level comment lines by construction. That run (blank
|
|
204
|
+
* and comment lines ending the file after a block with no END marker) is
|
|
205
|
+
* consumed as stale harness output when the source prefix, the generated
|
|
206
|
+
* header or the `# harness hook:` text appears anywhere in it, so an
|
|
207
|
+
* operator comment there that mentions such text mid-line is removed with
|
|
208
|
+
* the block.
|
|
209
|
+
*
|
|
210
|
+
* The returned probe answers for line starts queried in non-decreasing
|
|
211
|
+
* order (the top-level probe's contract, which a line that fails the text
|
|
212
|
+
* test never touches).
|
|
207
213
|
*/
|
|
208
|
-
function
|
|
214
|
+
function createMarkerLineProbe(text) {
|
|
215
|
+
const topLevel = createTopLevelProbe(text);
|
|
216
|
+
return (lineStart, marker, shape) => {
|
|
217
|
+
const trimmed = text.slice(lineStart, lineEndAfter(text, lineStart)).trim();
|
|
218
|
+
const textMatches = shape === "whole" ? trimmed === marker : trimmed.startsWith(marker);
|
|
219
|
+
return textMatches && topLevel(lineStart);
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/** The start of the first marker line (`createMarkerLineProbe`) at or after
|
|
223
|
+
* the line start `from`, or -1. Candidates are visited in order, one line
|
|
224
|
+
* each, so the probe's non-decreasing-offset contract holds. */
|
|
225
|
+
function findMarkerLine(text, marker, shape, from = 0) {
|
|
226
|
+
const isMarkerLine = createMarkerLineProbe(text);
|
|
209
227
|
let pos = from;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
pos = lineEnd;
|
|
228
|
+
for (;;) {
|
|
229
|
+
const idx = text.indexOf(marker, pos);
|
|
230
|
+
if (idx === -1)
|
|
231
|
+
return -1;
|
|
232
|
+
const lineStart = lineStartAt(text, idx);
|
|
233
|
+
if (isMarkerLine(lineStart, marker, shape))
|
|
234
|
+
return lineStart;
|
|
235
|
+
pos = lineEndAfter(text, lineStart);
|
|
219
236
|
}
|
|
220
|
-
|
|
237
|
+
}
|
|
238
|
+
/** True when a line in `[from, to)` is a marker line for the prefix
|
|
239
|
+
* `marker`. */
|
|
240
|
+
function rangeHoldsMarkerLine(text, from, to, marker, isMarkerLine) {
|
|
241
|
+
for (let pos = from; pos < to; pos = lineEndAfter(text, pos)) {
|
|
242
|
+
if (isMarkerLine(pos, marker, "prefix"))
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
221
246
|
}
|
|
222
247
|
/** Answers, for a line start in `text`, whether that line sits at the top
|
|
223
248
|
* level of the document, that is, not inside a multi-line array, a multi-line
|
|
@@ -399,6 +424,7 @@ function scanOwnedContentEnd(text, start) {
|
|
|
399
424
|
let sawOwnedContent = false;
|
|
400
425
|
let sawMarkedHookTable = false;
|
|
401
426
|
const classify = createLineClassifier(text);
|
|
427
|
+
const isMarkerLine = createMarkerLineProbe(text);
|
|
402
428
|
while (pos < text.length) {
|
|
403
429
|
const lineEnd = lineEndAfter(text, pos);
|
|
404
430
|
const rawLine = text.slice(pos, lineEnd);
|
|
@@ -409,7 +435,7 @@ function scanOwnedContentEnd(text, start) {
|
|
|
409
435
|
pos = lineEnd;
|
|
410
436
|
continue;
|
|
411
437
|
}
|
|
412
|
-
if (
|
|
438
|
+
if (isMarkerLine(pos, CODEX_MANAGED_END, "whole")) {
|
|
413
439
|
return { end: lineEnd, sawEndMarker: true };
|
|
414
440
|
}
|
|
415
441
|
if (kind === "comment") {
|
|
@@ -423,7 +449,7 @@ function scanOwnedContentEnd(text, start) {
|
|
|
423
449
|
}
|
|
424
450
|
if (kind === "hookHeader") {
|
|
425
451
|
const marked = commentRunStart !== null &&
|
|
426
|
-
|
|
452
|
+
rangeHoldsMarkerLine(text, commentRunStart, pos, HARNESS_HOOK_COMMENT_PREFIX, isMarkerLine);
|
|
427
453
|
if (marked) {
|
|
428
454
|
sawMarkedHookTable = true;
|
|
429
455
|
}
|
|
@@ -449,6 +475,11 @@ function scanOwnedContentEnd(text, start) {
|
|
|
449
475
|
if (commentRunStart !== null && sawOwnedContent) {
|
|
450
476
|
const run = text.slice(commentRunStart, pos);
|
|
451
477
|
const hasComment = /(^|\n)[ \t]*#/.test(run);
|
|
478
|
+
// Matched as substrings on purpose, outside `createMarkerLineProbe`:
|
|
479
|
+
// every line of this run is a whole comment or blank line at the top
|
|
480
|
+
// level (a string line resets the run, and at the end of a document that
|
|
481
|
+
// parses nothing is still open), so marker text here is never inside a
|
|
482
|
+
// value, and a comment that mentions a marker is stale harness output.
|
|
452
483
|
const harnessAuthored = run.includes(HARNESS_HOOK_COMMENT_PREFIX) ||
|
|
453
484
|
run.includes(CODEX_MANAGED_SOURCE_PREFIX) ||
|
|
454
485
|
run.includes(GENERATED_HEADER);
|
|
@@ -458,6 +489,33 @@ function scanOwnedContentEnd(text, start) {
|
|
|
458
489
|
}
|
|
459
490
|
return { end: pos, sawEndMarker: false };
|
|
460
491
|
}
|
|
492
|
+
/**
|
|
493
|
+
* True when a harness-commented hook table (a `# harness hook:` comment line
|
|
494
|
+
* followed, later in the zone, by a `[[hooks.` header or a hook event array
|
|
495
|
+
* header spelled without that prefix, `[[ hooks.SessionStart ]]`) lies after the named
|
|
496
|
+
* table's header line, before `to`. The comment is found as a marker line
|
|
497
|
+
* (`createMarkerLineProbe`), so a value that merely contains the marker text
|
|
498
|
+
* does not count. Used only to pick the fix guidance of a refusal (task
|
|
499
|
+
* 36d962d2).
|
|
500
|
+
*/
|
|
501
|
+
function harnessHookTableFollows(text, header, to) {
|
|
502
|
+
if (header === null)
|
|
503
|
+
return false;
|
|
504
|
+
let pos = lineEndAfter(text, header.start);
|
|
505
|
+
let sawComment = false;
|
|
506
|
+
const isMarkerLine = createMarkerLineProbe(text);
|
|
507
|
+
while (pos < to) {
|
|
508
|
+
const lineEnd = Math.min(lineEndAfter(text, pos), to);
|
|
509
|
+
const trimmed = text.slice(pos, lineEnd).trim();
|
|
510
|
+
if (isMarkerLine(pos, HARNESS_HOOK_COMMENT_PREFIX, "prefix"))
|
|
511
|
+
sawComment = true;
|
|
512
|
+
else if (sawComment && (HOOK_ARRAY_HEADER_RE.test(trimmed) || isHookEventArrayHeader(trimmed))) {
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
pos = lineEnd;
|
|
516
|
+
}
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
461
519
|
/**
|
|
462
520
|
* Refuses loudly (mirror case of the stray-END bug) when the zone between
|
|
463
521
|
* `from` and `to` still contains a recognizable fragment of a harness
|
|
@@ -471,7 +529,8 @@ function scanOwnedContentEnd(text, start) {
|
|
|
471
529
|
* `scanOwnedContentEnd`; when only blank or comment lines precede the found
|
|
472
530
|
* marker in the zone -- so the pre-marker lookup finds no content line to
|
|
473
531
|
* report -- the header is looked up again just past that marker's own
|
|
474
|
-
* line, since the wedged table then sits after it, not before
|
|
532
|
+
* line, since the wedged table then sits after it, not before, and the
|
|
533
|
+
* message says the marker line sits above the table (task e8f4fc03); printed
|
|
475
534
|
* through `describeTableHeader`, so only the header's own bracketed key
|
|
476
535
|
* path appears, never a trailing comment), and
|
|
477
536
|
* the line number and marker of the first line beyond it
|
|
@@ -485,14 +544,33 @@ function scanOwnedContentEnd(text, start) {
|
|
|
485
544
|
* document (`to` is `text.length`), telling the operator to move something
|
|
486
545
|
* below a marker that does not exist would be nonsensical, so a different
|
|
487
546
|
* fix (add one, or move the table below the last harness hook table) is
|
|
488
|
-
* offered instead (task 6a037359).
|
|
547
|
+
* offered instead (task 6a037359). Adding one is offered only when no
|
|
548
|
+
* harness-commented hook table follows the named table
|
|
549
|
+
* (`harnessHookTableFollows`): an END placed before the named table would
|
|
550
|
+
* leave that later table running next to the fresh block (task 36d962d2).
|
|
551
|
+
* When the named table is a hook event table spelled without the `[[hooks.`
|
|
552
|
+
* prefix, the guidance offers deleting it if harness wrote it, since the scan
|
|
553
|
+
* reads such a table as foreign with or without a comment line above it
|
|
554
|
+
* (task e8f4fc03). The legacy paths call
|
|
555
|
+
* this too, so the substring matching above applies there as well: a value
|
|
556
|
+
* holding marker text refuses. This candidate list and the trailing comment
|
|
557
|
+
* run of `scanOwnedContentEnd` are the two marker searches that do not go
|
|
558
|
+
* through `createMarkerLineProbe`, on purpose. This one can only refuse,
|
|
559
|
+
* never install, so matching marker text in a value costs a refusal, never
|
|
560
|
+
* operator content. The comment run's lines are whole top-level comment
|
|
561
|
+
* lines, so marker text there is never inside a value, but that run (blank
|
|
562
|
+
* and comment lines ending the file after a block with no END marker) is
|
|
563
|
+
* consumed when the source prefix, the generated header or the
|
|
564
|
+
* `# harness hook:` text appears anywhere in it, so an operator comment
|
|
565
|
+
* there that mentions such text mid-line is removed with the block.
|
|
566
|
+
* `blockOpening` names the line the block
|
|
489
567
|
* starts at in the delete-the-lines fix: the BEGIN marker by default; a
|
|
490
568
|
* legacy config (no BEGIN marker) whose END marker survived passes its
|
|
491
569
|
* source-prefix line or generated header instead, since a BEGIN marker it
|
|
492
570
|
* does not have is no line to delete from (task 01053b27). For the generated
|
|
493
571
|
* header the fix deletes that line and the END marker line too: a generated
|
|
494
572
|
* header left alone above END no longer marks a harness block
|
|
495
|
-
* (`findManagedRange` needs a `[[hooks.` line after it), so the install
|
|
573
|
+
* (`findManagedRange` needs a top-level `[[hooks.` header line after it), so the install
|
|
496
574
|
* would add a fresh block and leave that pair behind as an orphan. Exported for
|
|
497
575
|
* direct tests only: the scan
|
|
498
576
|
* that feeds it never hands it a line that is not a real header, so its own
|
|
@@ -526,9 +604,15 @@ export function assertNoSplitBlock(text, from, to, configPath, hasEndMarker, blo
|
|
|
526
604
|
// the foreign table that split the block is wedged after the marker's own
|
|
527
605
|
// line instead, so look there.
|
|
528
606
|
const markerLineEnd = lineEndAfter(text, zoneStart + markerOffset);
|
|
529
|
-
const
|
|
530
|
-
|
|
607
|
+
const headerBeforeMarker = firstContentLine(text, zoneStart, zoneStart + markerOffset);
|
|
608
|
+
const foreignHeaderLine = headerBeforeMarker ?? firstContentLine(text, markerLineEnd, zoneEnd);
|
|
531
609
|
const firstForeignHeader = describeForeignHeaderLine(text, foreignHeaderLine);
|
|
610
|
+
// Where the marker line sits relative to the named table, so the message
|
|
611
|
+
// stays true when the table was found past the marker (a respelled hook
|
|
612
|
+
// table with its own '# harness hook:' comment line above it, say).
|
|
613
|
+
const placement = headerBeforeMarker === null
|
|
614
|
+
? `below harness-owned content: line ${offendingLineNumber}, above it, contains '${marker}'`
|
|
615
|
+
: `sitting before more harness-owned content: line ${offendingLineNumber} contains '${marker}'`;
|
|
532
616
|
const fixes = hasEndMarker
|
|
533
617
|
? [
|
|
534
618
|
`move ${firstForeignHeader} below the '${CODEX_MANAGED_END}' marker`,
|
|
@@ -538,23 +622,37 @@ export function assertNoSplitBlock(text, from, to, configPath, hasEndMarker, blo
|
|
|
538
622
|
: `delete the lines between ${blockOpening} and '${CODEX_MANAGED_END}' so only ` +
|
|
539
623
|
"harness-owned tables remain between them",
|
|
540
624
|
]
|
|
541
|
-
:
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
625
|
+
: harnessHookTableFollows(text, foreignHeaderLine, zoneEnd)
|
|
626
|
+
? // An END marker placed before the named table would leave the
|
|
627
|
+
// harness-commented hook table after it running next to the fresh
|
|
628
|
+
// block, so the only fix offered is moving the named table past it
|
|
629
|
+
// (task 36d962d2).
|
|
630
|
+
[`move ${firstForeignHeader} below the last harness hook table`]
|
|
631
|
+
: [
|
|
632
|
+
`add a '${CODEX_MANAGED_END}' marker line right after the last harness hook table ` +
|
|
633
|
+
`(before ${firstForeignHeader})`,
|
|
634
|
+
`move ${firstForeignHeader} below the last harness hook table`,
|
|
635
|
+
];
|
|
546
636
|
// An uncommented hook table in the middle may be a harness table whose
|
|
547
637
|
// comment line was deleted, so moving it (the operator-table fix) is only
|
|
548
638
|
// right for an operator-owned one; name the restore option first (task
|
|
549
639
|
// 01053b27).
|
|
550
640
|
const isHookTable = HOOK_ARRAY_HEADER_RE.test(foreignHeaderLine?.line ?? "");
|
|
641
|
+
// A hook event table spelled another way (`[[ hooks.Stop ]]`) is read as
|
|
642
|
+
// foreign with or without a comment line above it, so restoring one does
|
|
643
|
+
// not help; deleting it does, if harness wrote it (task e8f4fc03).
|
|
644
|
+
const isRespelledHookTable = isHookEventArrayHeader(foreignHeaderLine?.line ?? "");
|
|
551
645
|
const guidance = isHookTable
|
|
552
646
|
? `If ${firstForeignHeader} is a harness hook table whose '${HARNESS_HOOK_COMMENT_PREFIX}<id> ` +
|
|
553
647
|
"(budget_ms=<n>)' comment line was deleted, restore that comment line directly above it. " +
|
|
554
648
|
`If it is an operator-owned table, ${fixes.join(", or ")}`
|
|
555
|
-
:
|
|
556
|
-
|
|
557
|
-
|
|
649
|
+
: isRespelledHookTable
|
|
650
|
+
? `If ${firstForeignHeader} is a harness hook table, delete it together with any ` +
|
|
651
|
+
`'${HARNESS_HOOK_COMMENT_PREFIX}<id> (budget_ms=<n>)' comment line above it (the install ` +
|
|
652
|
+
"replaces only hook tables spelled '[[hooks.<Event>]]' with that comment line above " +
|
|
653
|
+
`them, and writes the fresh block). If it is an operator-owned table, ${fixes.join(", or ")}`
|
|
654
|
+
: fixes.join(", or ").replace(/^./, (c) => c.toUpperCase());
|
|
655
|
+
throw new CodexInstallRefusalError(`Codex config ${configPath} has a foreign table (${firstForeignHeader}) ${placement} and still ` +
|
|
558
656
|
"looks like part of the harness-managed hook block; refusing to guess the block " +
|
|
559
657
|
`boundaries. ${guidance}, then re-run \`harness apply --runtime codex --install\`. As a ` +
|
|
560
658
|
`last resort, restore ${configPath} from the most recent .harness-backup-* file (if one exists), but ` +
|
|
@@ -638,18 +736,22 @@ function foreignSectionRootKey(headerLine) {
|
|
|
638
736
|
* so the operator reads "a whole namespace, N tables" rather than
|
|
639
737
|
* mistaking the printed header for the only table kept; exactly one table
|
|
640
738
|
* under a root prints its own header. Headers print through
|
|
641
|
-
* `describeTableHeader` (no trailing comment).
|
|
642
|
-
|
|
739
|
+
* `describeTableHeader` (no trailing comment). A header that is one of the
|
|
740
|
+
* `kept` operator hook tables (a hook event table spelled without the
|
|
741
|
+
* `[[hooks.` prefix, see `collectKeptHookTables`) is named there instead and
|
|
742
|
+
* not listed twice. */
|
|
743
|
+
function collectForeignSectionHeaders(text, from, to, kept = []) {
|
|
643
744
|
const seenRoots = new Set();
|
|
644
745
|
const headers = [];
|
|
645
746
|
const counts = new Map();
|
|
747
|
+
const keptStarts = new Set(kept.map((t) => t.start));
|
|
646
748
|
let pos = Math.max(from, 0);
|
|
647
749
|
const limit = Math.max(to, from);
|
|
648
750
|
const classify = createLineClassifier(text);
|
|
649
751
|
while (pos < limit) {
|
|
650
752
|
const lineEnd = Math.min(lineEndAfter(text, pos), limit);
|
|
651
753
|
const rawLine = text.slice(pos, lineEnd);
|
|
652
|
-
if (classify(pos, rawLine) === "foreignHeader") {
|
|
754
|
+
if (classify(pos, rawLine) === "foreignHeader" && !keptStarts.has(pos)) {
|
|
653
755
|
const trimmed = rawLine.trim();
|
|
654
756
|
const root = foreignSectionRootKey(trimmed);
|
|
655
757
|
counts.set(root, (counts.get(root) ?? 0) + 1);
|
|
@@ -666,27 +768,44 @@ function collectForeignSectionHeaders(text, from, to) {
|
|
|
666
768
|
return count > 1 ? `[${root}.*] (${count} tables)` : header;
|
|
667
769
|
});
|
|
668
770
|
}
|
|
669
|
-
|
|
771
|
+
/** The hook ids named by the `# harness hook:` marker lines
|
|
772
|
+
* (`createMarkerLineProbe`) in `[from, to)` of `text`, so a value holding
|
|
773
|
+
* that text is never reported as a removed hook. */
|
|
774
|
+
function extractHookIds(text, from = 0, to = text.length) {
|
|
670
775
|
const ids = new Set();
|
|
671
|
-
|
|
672
|
-
|
|
776
|
+
const isMarkerLine = createMarkerLineProbe(text);
|
|
777
|
+
for (let pos = from; pos < to; pos = lineEndAfter(text, pos)) {
|
|
778
|
+
if (!isMarkerLine(pos, HARNESS_HOOK_COMMENT_PREFIX, "prefix"))
|
|
779
|
+
continue;
|
|
780
|
+
const m = HOOK_ID_LINE_RE.exec(text.slice(pos, lineEndAfter(text, pos)).trim());
|
|
673
781
|
if (m?.[1] !== undefined)
|
|
674
782
|
ids.add(m[1]);
|
|
675
783
|
}
|
|
676
784
|
return ids;
|
|
677
785
|
}
|
|
678
786
|
/**
|
|
679
|
-
* The operator-authored hook tables directly after a managed range
|
|
680
|
-
*
|
|
681
|
-
* `
|
|
682
|
-
* `
|
|
787
|
+
* The operator-authored hook tables directly after a managed range: every
|
|
788
|
+
* hook event table header from `from` up to the first non-hook table header
|
|
789
|
+
* or `to`, each by header, line number and offset. A hook event table is a
|
|
790
|
+
* `[[hooks.*]]` header, or an array-of-tables header spelled another way
|
|
791
|
+
* whose first key resolves to `hooks` and that names an event
|
|
792
|
+
* (`isHookEventArrayHeader`: `[[ hooks.Stop ]]`, `[["hooks".Stop]]`,
|
|
793
|
+
* `[[hooks . Stop]]`). The ownership scan reads such a respelled header as a
|
|
794
|
+
* foreign table, so it ends the managed range there; collected here, it is
|
|
795
|
+
* named and its commands are checked like any kept hook table, instead of
|
|
796
|
+
* being kept as a foreign section whose harness command would then run next
|
|
797
|
+
* to the fresh block's (task e8f4fc03). Lines are read by
|
|
683
798
|
* `createLineClassifier`, so an array element on its own line inside a kept
|
|
684
799
|
* table's multi-line value neither ends the run nor is named, and a hook
|
|
685
|
-
* table after it is still collected (task 01053b27). Empty
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
800
|
+
* table after it is still collected (task 01053b27). Empty when the first
|
|
801
|
+
* header at `from` is a non-hook table, or there is none. A hook table after
|
|
802
|
+
* the first non-hook table is not collected: it is kept, not named and not
|
|
803
|
+
* checked, a deliberate boundary (only the run of hook tables directly after
|
|
804
|
+
* the harness tables is examined). The caller refuses a zone that holds a
|
|
805
|
+
* harness marker (`assertNoSplitBlock`) before it uses the tables: over the
|
|
806
|
+
* whole zone on the BEGIN path and when an END marker follows, over the run
|
|
807
|
+
* of hook tables (up to the returned `end`) on the legacy path without one,
|
|
808
|
+
* so no table the install keeps carries a `# harness hook:` comment.
|
|
690
809
|
*/
|
|
691
810
|
function collectKeptHookTables(text, from, to) {
|
|
692
811
|
const tables = [];
|
|
@@ -696,14 +815,15 @@ function collectKeptHookTables(text, from, to) {
|
|
|
696
815
|
const lineEnd = Math.min(lineEndAfter(text, pos), to);
|
|
697
816
|
const rawLine = text.slice(pos, lineEnd);
|
|
698
817
|
const kind = classify(pos, rawLine);
|
|
699
|
-
|
|
818
|
+
const hookTable = kind === "hookHeader" || (kind === "foreignHeader" && isHookEventArrayHeader(rawLine.trim()));
|
|
819
|
+
if (kind === "foreignHeader" && !hookTable)
|
|
700
820
|
break;
|
|
701
|
-
if (
|
|
821
|
+
if (hookTable) {
|
|
702
822
|
tables.push({ header: describeTableHeader(rawLine), line: lineNumberAt(text, pos), start: pos });
|
|
703
823
|
}
|
|
704
824
|
pos = lineEnd;
|
|
705
825
|
}
|
|
706
|
-
return tables;
|
|
826
|
+
return { tables, end: pos };
|
|
707
827
|
}
|
|
708
828
|
/** A kept table as the install names it: its header, its 1-based line
|
|
709
829
|
* number in the config before the install and in the config the install
|
|
@@ -729,19 +849,19 @@ function startPastBom(text, start) {
|
|
|
729
849
|
* `blockOpening` is the line the split-block refusal names as the start of
|
|
730
850
|
* the block (`assertNoSplitBlock`; the BEGIN marker when omitted).
|
|
731
851
|
*/
|
|
732
|
-
function rangeBeforeStrayEnd(text, start, end, configPath, blockOpening) {
|
|
733
|
-
const strayIdx =
|
|
852
|
+
function rangeBeforeStrayEnd(text, start, end, configPath, blockOpening, zoneFrom = end) {
|
|
853
|
+
const strayIdx = findMarkerLine(text, CODEX_MANAGED_END, "whole", end);
|
|
734
854
|
if (strayIdx === -1) {
|
|
735
855
|
assertNoSplitBlock(text, end, text.length, configPath, false);
|
|
736
|
-
const
|
|
737
|
-
const
|
|
856
|
+
const keptHookTables = collectKeptHookTables(text, end, text.length).tables;
|
|
857
|
+
const foreignSectionsPreserved = collectForeignSectionHeaders(text, end, text.length, keptHookTables);
|
|
738
858
|
return { start, end, foreignSectionsPreserved, keptHookTables };
|
|
739
859
|
}
|
|
740
860
|
const strayLineStart = lineStartAt(text, strayIdx);
|
|
741
861
|
const strayLineEnd = lineEndAfter(text, strayIdx);
|
|
742
|
-
assertNoSplitBlock(text,
|
|
743
|
-
const
|
|
744
|
-
const
|
|
862
|
+
assertNoSplitBlock(text, zoneFrom, strayLineStart, configPath, true, blockOpening);
|
|
863
|
+
const keptHookTables = collectKeptHookTables(text, end, strayLineStart).tables;
|
|
864
|
+
const foreignSectionsPreserved = collectForeignSectionHeaders(text, end, strayLineStart, keptHookTables);
|
|
745
865
|
return {
|
|
746
866
|
start,
|
|
747
867
|
end,
|
|
@@ -753,39 +873,73 @@ function rangeBeforeStrayEnd(text, start, end, configPath, blockOpening) {
|
|
|
753
873
|
/**
|
|
754
874
|
* The range of a legacy (no BEGIN marker) config: the source-prefix or
|
|
755
875
|
* generated-header paths. When the scan ended at an operator hook table,
|
|
756
|
-
*
|
|
757
|
-
* (`rangeBeforeStrayEnd`): an END marker that
|
|
758
|
-
* line, sitting after the kept tables
|
|
759
|
-
*
|
|
760
|
-
* kept-table refusal offers moving an operator-owned
|
|
761
|
-
* a harness-commented table after the kept tables
|
|
762
|
-
* split-block check instead of being
|
|
763
|
-
* (the fresh block would repeat it)
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
876
|
+
* or at a foreign table with an END marker after it, the zone after it is
|
|
877
|
+
* handled like the BEGIN path's (`rangeBeforeStrayEnd`): an END marker that
|
|
878
|
+
* survived its deleted BEGIN line, sitting after the kept tables or the
|
|
879
|
+
* foreign table, is excised like a drifted END, so the install leaves no
|
|
880
|
+
* orphan END line and the kept-table refusal offers moving an operator-owned
|
|
881
|
+
* table below that END; a harness-commented table after the kept tables or
|
|
882
|
+
* the foreign table refuses through the split-block check instead of being
|
|
883
|
+
* kept as if it were operator content (the fresh block would repeat it)
|
|
884
|
+
* when it sits before that END, or, after kept tables, anywhere later when
|
|
885
|
+
* no END follows (task e8f4fc03 extends the END case to a foreign table).
|
|
886
|
+
* Content below that END is untouched, as on the BEGIN path, so a
|
|
887
|
+
* harness-commented table there is kept and not named. The split-block
|
|
888
|
+
* refusal names `blockOpening` (the source-prefix line or the generated
|
|
889
|
+
* header) as the start of the block, since there is no BEGIN marker (task
|
|
890
|
+
* 01053b27); the split-block zone starts after that opening line, so a block
|
|
891
|
+
* with no hook table in it (the source prefix directly above an operator
|
|
892
|
+
* table) is not refused for its own opening line (task 36d962d2). The opening
|
|
893
|
+
* line is a marker line (`createMarkerLineProbe`), so marker text inside a
|
|
894
|
+
* value or after a content line opens no block.
|
|
895
|
+
* A scan that ended at a foreign table with no END marker anywhere after it
|
|
896
|
+
* keeps the legacy behaviour (no split-block check), except that hook event tables directly after the block are collected, named and
|
|
897
|
+
* checked (`collectKeptHookTables`), and a harness marker in their run
|
|
898
|
+
* refuses through the split-block check. The END marker is looked up as a
|
|
899
|
+
* marker line too, so END text in a foreign table's value does not route a
|
|
900
|
+
* config here. Like the BEGIN path,
|
|
901
|
+
* the split-block check matches its markers as substrings, so a value in the
|
|
902
|
+
* zone that holds marker text refuses too (see `assertNoSplitBlock`).
|
|
769
903
|
*/
|
|
770
904
|
function legacyManagedRange(text, start, scan, configPath, blockOpening) {
|
|
771
|
-
if (scan.
|
|
772
|
-
return
|
|
905
|
+
if (scan.sawEndMarker) {
|
|
906
|
+
return { start, end: scan.end, foreignSectionsPreserved: [], keptHookTables: [] };
|
|
773
907
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
908
|
+
// The split-block zone starts after the block's own opening line: when the
|
|
909
|
+
// scan owned nothing (the generated header or a foreign table directly
|
|
910
|
+
// follows the source prefix), `scan.end` is that opening line itself, which
|
|
911
|
+
// is a split-block marker and would refuse a block with nothing in it
|
|
912
|
+
// (task 36d962d2).
|
|
913
|
+
const zoneFrom = Math.max(scan.end, lineEndAfter(text, start));
|
|
914
|
+
if (scan.endedAtOperatorHookTable || findMarkerLine(text, CODEX_MANAGED_END, "whole", scan.end) !== -1) {
|
|
915
|
+
return rangeBeforeStrayEnd(text, start, scan.end, configPath, blockOpening, zoneFrom);
|
|
916
|
+
}
|
|
917
|
+
const run = collectKeptHookTables(text, scan.end, text.length);
|
|
918
|
+
// The scan ended at a hook table spelled without the `[[hooks.` prefix: a
|
|
919
|
+
// harness marker in the run of hook tables after it (a harness-commented
|
|
920
|
+
// table, or the respelled table's own comment line) is harness content the
|
|
921
|
+
// scan could not read, so refuse through the split-block check, as the
|
|
922
|
+
// BEGIN path does, instead of naming a table with that comment line above it
|
|
923
|
+
// as one without (task e8f4fc03). The run stops at the first non-hook table:
|
|
924
|
+
// past it the legacy reading below stays unchanged.
|
|
925
|
+
if (run.tables.length > 0)
|
|
926
|
+
assertNoSplitBlock(text, zoneFrom, run.end, configPath, false);
|
|
927
|
+
const keptHookTables = run.tables;
|
|
928
|
+
const foreignSectionsPreserved = collectForeignSectionHeaders(text, scan.end, text.length, keptHookTables);
|
|
929
|
+
return { start, end: scan.end, foreignSectionsPreserved, keptHookTables };
|
|
778
930
|
}
|
|
779
931
|
function findManagedRange(text, configPath) {
|
|
780
|
-
|
|
932
|
+
// BEGIN is found as a marker line (`createMarkerLineProbe`): BEGIN text in
|
|
933
|
+
// an operator value (a string, a trailing comment) is neither the block nor
|
|
934
|
+
// a second marker, so the install neither deletes operator content after
|
|
935
|
+
// it nor splices the fresh block into the value (task 36d962d2).
|
|
936
|
+
const begin = findMarkerLine(text, CODEX_MANAGED_BEGIN, "prefix");
|
|
781
937
|
if (begin !== -1) {
|
|
782
|
-
const secondBegin = text
|
|
938
|
+
const secondBegin = findMarkerLine(text, CODEX_MANAGED_BEGIN, "prefix", lineEndAfter(text, begin));
|
|
783
939
|
if (secondBegin !== -1) {
|
|
784
940
|
// Names the second marker's line number only, never that line's own
|
|
785
|
-
// text
|
|
786
|
-
|
|
787
|
-
// the marker text, and this refusal is printed to stderr and `--json`.
|
|
788
|
-
const secondBeginLineNumber = lineNumberAt(text, lineStartAt(text, secondBegin));
|
|
941
|
+
// text, since this refusal is printed to stderr and `--json`.
|
|
942
|
+
const secondBeginLineNumber = lineNumberAt(text, secondBegin);
|
|
789
943
|
throw new CodexInstallRefusalError(`Codex config ${configPath} has more than one '${CODEX_MANAGED_BEGIN}' marker ` +
|
|
790
944
|
`(a second one at line ${secondBeginLineNumber}); refusing to ` +
|
|
791
945
|
"guess which one is current. Delete the entire unwanted BEGIN/END pair -- including " +
|
|
@@ -796,14 +950,14 @@ function findManagedRange(text, configPath) {
|
|
|
796
950
|
"but that discards any Codex state (hook trust entries, marketplaces, plugins) " +
|
|
797
951
|
"written to the file since that backup.", configPath);
|
|
798
952
|
}
|
|
799
|
-
let start =
|
|
953
|
+
let start = begin;
|
|
800
954
|
const prev = previousLine(text, start);
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
955
|
+
// Only a marker line is the block's source prefix: the closing line of a
|
|
956
|
+
// multi-line string that ends with the prefix text is the operator's
|
|
957
|
+
// value, not part of the block (task 36d962d2). Trimming also drops a BOM
|
|
958
|
+
// sitting before the source-prefix comment on line 1 (task b34ed105).
|
|
959
|
+
if (prev && createMarkerLineProbe(text)(prev.start, CODEX_MANAGED_SOURCE_PREFIX, "prefix")) {
|
|
960
|
+
start = prev.start;
|
|
807
961
|
}
|
|
808
962
|
start = startPastBom(text, start);
|
|
809
963
|
const contentStart = lineEndAfter(text, begin);
|
|
@@ -814,19 +968,24 @@ function findManagedRange(text, configPath) {
|
|
|
814
968
|
}
|
|
815
969
|
return rangeBeforeStrayEnd(text, start, end, configPath);
|
|
816
970
|
}
|
|
817
|
-
const source = text
|
|
971
|
+
const source = findMarkerLine(text, CODEX_MANAGED_SOURCE_PREFIX, "prefix");
|
|
818
972
|
if (source !== -1) {
|
|
819
|
-
const start = startPastBom(text,
|
|
973
|
+
const start = startPastBom(text, source);
|
|
820
974
|
return legacyManagedRange(text, start, scanOwnedContentEnd(text, start), configPath, `the '${CODEX_MANAGED_SOURCE_PREFIX}' source-prefix line`);
|
|
821
975
|
}
|
|
822
|
-
const generated = text
|
|
976
|
+
const generated = findMarkerLine(text, GENERATED_HEADER, "prefix");
|
|
823
977
|
if (generated !== -1) {
|
|
824
|
-
const start = startPastBom(text,
|
|
978
|
+
const start = startPastBom(text, generated);
|
|
825
979
|
const scan = scanOwnedContentEnd(text, start);
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
980
|
+
// The generated header opens a legacy block only when a hook table header
|
|
981
|
+
// line (`[[hooks.`, the older `[[hooks.pre_tool_use]]` included) follows
|
|
982
|
+
// it inside the scanned range, found by the same top-level line rule as
|
|
983
|
+
// the markers: `[[hooks.` text in a value, a multi-line string line or a
|
|
984
|
+
// comment under an operator table is operator content, and taking it for
|
|
985
|
+
// a header made the install replace the operator's keys after the header
|
|
986
|
+
// line with the fresh block (task 36d962d2).
|
|
987
|
+
const hookHeader = findMarkerLine(text, "[[hooks.", "prefix", start);
|
|
988
|
+
if (hookHeader !== -1 && hookHeader < scan.end) {
|
|
830
989
|
return legacyManagedRange(text, start, scan, configPath, GENERATED_HEADER_OPENING);
|
|
831
990
|
}
|
|
832
991
|
}
|
|
@@ -1189,6 +1348,17 @@ function resolvedKeyName(segment) {
|
|
|
1189
1348
|
function hooksRooted(trimmed) {
|
|
1190
1349
|
return resolvedKeyName(parseTableHeader(trimmed)?.keys[0] ?? "") === "hooks";
|
|
1191
1350
|
}
|
|
1351
|
+
/** Whether a header line is an array-of-tables header that appends to a
|
|
1352
|
+
* hook event array or below one (`[[hooks.Stop]]`, `[[ hooks.Stop ]]`,
|
|
1353
|
+
* `[["hooks".Stop]]`, `[[hooks . Stop.hooks]]`): double brackets, a first key
|
|
1354
|
+
* that resolves to `hooks` (`hooksRooted`) and at least an event key after
|
|
1355
|
+
* it. A one-key line (`[["hooks"]]`, which may also be an array element on
|
|
1356
|
+
* its own line) is not one. Used by `collectKeptHookTables` for the
|
|
1357
|
+
* spellings the `[[hooks.` prefix does not catch (task e8f4fc03). */
|
|
1358
|
+
function isHookEventArrayHeader(trimmed) {
|
|
1359
|
+
const parsed = parseTableHeader(trimmed);
|
|
1360
|
+
return (parsed !== null && parsed.header.startsWith("[[") && parsed.keys.length >= 2 && hooksRooted(trimmed));
|
|
1361
|
+
}
|
|
1192
1362
|
function subKey(event, index) {
|
|
1193
1363
|
return `${event}\u0000${index}`;
|
|
1194
1364
|
}
|
|
@@ -1210,11 +1380,12 @@ function subKey(event, index) {
|
|
|
1210
1380
|
* scan resolves has the root `hooks` (a `[[hooks.` line has it by its
|
|
1211
1381
|
* prefix), a table outside `hooks` never shifts the order match, and an
|
|
1212
1382
|
* array element on its own line (`["x"]`, `[1]`) costs no parse in this
|
|
1213
|
-
* scan, unless the element
|
|
1214
|
-
* `['hooks']
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1217
|
-
* finds it inside the
|
|
1383
|
+
* scan, unless the element line reads as a header whose first key is
|
|
1384
|
+
* `hooks` (`["hooks"]`, `['hooks']`, `[["hooks"]]`, `[[ 'hooks' ]]`): that
|
|
1385
|
+
* line reads as a `hooks` root, so it is probed like the `hooks`-rooted
|
|
1386
|
+
* spellings that `[[hooks.` does not catch (`[[ hooks.Stop ]]`,
|
|
1387
|
+
* `[["hooks".Stop]]`, `[hooks.Stop]`), and the probe finds it inside the
|
|
1388
|
+
* array.
|
|
1218
1389
|
*/
|
|
1219
1390
|
function mapHookArrayHeaders(text) {
|
|
1220
1391
|
const map = { slots: new Map(), eventCounts: new Map(), subCounts: new Map() };
|
|
@@ -1375,14 +1546,22 @@ function assertKeptHookTablesAreOperatorOwned(kept, currentContent, generatedCon
|
|
|
1375
1546
|
const harnessShaped = commands.some((command) => freshCommands.has(command) || command.startsWith("harness "));
|
|
1376
1547
|
if (!harnessShaped)
|
|
1377
1548
|
continue;
|
|
1549
|
+
// harness writes every hook table as `[[hooks.<Event>]]`; a table spelled
|
|
1550
|
+
// another way (`[[ hooks.Stop ]]`) never was one, and with the comment
|
|
1551
|
+
// line restored above it the scan would still read it as a foreign
|
|
1552
|
+
// table, so the restore fix is offered only for the harness spelling
|
|
1553
|
+
// (task e8f4fc03).
|
|
1554
|
+
const fix = HOOK_ARRAY_HEADER_RE.test(table.header)
|
|
1555
|
+
? `Restore the comment line directly above ${table.header}, so the install replaces the table ` +
|
|
1556
|
+
`with the fresh block${moveBelowEndOption(hasEndMarker)}, or delete the table`
|
|
1557
|
+
: "Delete the table (the install replaces only hook tables spelled '[[hooks.<Event>]]' with " +
|
|
1558
|
+
`that comment line above them)${moveBelowEndOption(hasEndMarker)}`;
|
|
1378
1559
|
throw new CodexInstallRefusalError(`Codex config ${configPath} has a hook table ${table.header} (line ${table.line}) with no ` +
|
|
1379
1560
|
`'${HARNESS_HOOK_COMMENT_PREFIX}<id> (budget_ms=<n>)' comment line above it, placed after the ` +
|
|
1380
1561
|
"harness hook tables, whose command is one harness itself writes. harness cannot tell it " +
|
|
1381
1562
|
"from a harness hook table whose comment line was deleted, and keeping it next to the " +
|
|
1382
1563
|
"fresh harness block would run that hook twice (or keep a retired harness hook running). " +
|
|
1383
|
-
|
|
1384
|
-
`with the fresh block${moveBelowEndOption(hasEndMarker)}, or delete the table, then re-run ` +
|
|
1385
|
-
`\`harness apply --runtime codex --install\`. The file is untouched.`, configPath);
|
|
1564
|
+
`${fix}, then re-run \`harness apply --runtime codex --install\`. The file is untouched.`, configPath);
|
|
1386
1565
|
}
|
|
1387
1566
|
}
|
|
1388
1567
|
export function planCodexConfigInstall(opts) {
|
|
@@ -1412,7 +1591,7 @@ export function planCodexConfigInstall(opts) {
|
|
|
1412
1591
|
let keptTableDescriptions = [];
|
|
1413
1592
|
let hasEndMarkerAfterKept = false;
|
|
1414
1593
|
if (range) {
|
|
1415
|
-
const oldHookIds = extractHookIds(currentContent
|
|
1594
|
+
const oldHookIds = extractHookIds(currentContent, range.start, range.end);
|
|
1416
1595
|
const newHookIds = extractHookIds(managedBlock);
|
|
1417
1596
|
removedHookIds = [...oldHookIds].filter((id) => !newHookIds.has(id));
|
|
1418
1597
|
keptHookTables = range.keptHookTables;
|