harness-everything 0.30.9 → 0.30.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.
@@ -370,15 +370,16 @@ function routerState(payload) {
370
370
  }
371
371
  }
372
372
 
373
- function withLock(root, fn) {
373
+ function withLock(root, fn, options = {}) {
374
374
  fs.mkdirSync(root, { recursive: true, mode: 0o700 });
375
375
  const lock = path.join(root, '.index.lock');
376
- const deadline = Date.now() + 2000;
376
+ const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs >= 0 ? options.timeoutMs : 2000;
377
+ const deadline = Date.now() + timeoutMs;
377
378
  let fd = null;
378
379
  while (fd === null) {
379
380
  try { fd = fs.openSync(lock, 'wx'); } catch (_) {
380
381
  try { if (Date.now() - fs.statSync(lock).mtimeMs > 5000) fs.unlinkSync(lock); } catch (__) { /* raced */ }
381
- if (Date.now() > deadline) return false;
382
+ if (Date.now() >= deadline) return false;
382
383
  const until = Date.now() + 10;
383
384
  while (Date.now() < until) { /* short spin; hooks are short-lived */ }
384
385
  }
@@ -432,18 +433,88 @@ function sweep(root, now = Date.now()) {
432
433
  });
433
434
  }
434
435
 
435
- function appendRecord(root, record, text) {
436
- return withLock(root, () => {
436
+ function appendRecordDetailed(root, record, text, options = {}) {
437
+ let outcome = null;
438
+ const locked = withLock(root, () => {
439
+ const file = path.join(root, 'observations-index.json');
440
+ const index = readJson(file, null) || { schemaVersion: 1, kind: 'system-one-observations', records: [] };
441
+ if (options.dedupeById && index.records.some(existing => existing.id === record.id)) {
442
+ outcome = 'duplicate';
443
+ return;
444
+ }
445
+ if (options.dedupeByTurn) {
446
+ const match = observationTurnMatch(index.records, record);
447
+ if (match) {
448
+ outcome = match === 'ambiguous-legacy-turn' ? 'ambiguous' : 'duplicate';
449
+ return;
450
+ }
451
+ }
437
452
  // The blob is written under the lock, so a concurrent sweep cannot remove it before its record lands.
438
453
  const textDir = path.join(root, 'text');
439
454
  fs.mkdirSync(textDir, { recursive: true, mode: 0o700 });
440
- if (text) fs.writeFileSync(path.join(textDir, `${record.contentSha256}.json`), JSON.stringify(text), { mode: 0o600 });
441
- const file = path.join(root, 'observations-index.json');
442
- const index = readJson(file, null) || { schemaVersion: 1, kind: 'system-one-observations', records: [] };
455
+ if (text) fs.writeFileSync(path.join(textDir, record.contentSha256 + '.json'), JSON.stringify(text), { mode: 0o600 });
443
456
  index.records.push(record);
444
457
  sweepLocked(root, index, Date.now());
445
458
  writeJsonAtomic(file, index);
446
- });
459
+ outcome = 'appended';
460
+ }, { timeoutMs: options.lockTimeoutMs });
461
+ if (!locked) return { status: 'lock-timeout' };
462
+ return { status: outcome || 'appended' };
463
+ }
464
+
465
+ function appendRecord(root, record, text, options = {}) {
466
+ return appendRecordDetailed(root, record, text, options).status === 'appended';
467
+ }
468
+
469
+ function observationTurnMatch(records, record) {
470
+ const writer = record && record.writer || {};
471
+ const sessionId = String(writer.sessionId || '');
472
+ const turnId = String(writer.turnId || '');
473
+ if (!sessionId || !record.host) return null;
474
+ const sameSession = (Array.isArray(records) ? records : []).filter(existing =>
475
+ existing && existing.host === record.host &&
476
+ String(existing.writer && existing.writer.sessionId || '') === sessionId
477
+ );
478
+ if (!sameSession.length) return null;
479
+
480
+ if (turnId && sameSession.some(existing => String(existing.writer && existing.writer.turnId || '') === turnId)) {
481
+ return 'same-turn-id';
482
+ }
483
+
484
+ // Older live records have no turn ID. Only treat matching ordinal plus text
485
+ // as a definite duplicate; a partial match is ambiguous and the importer
486
+ // skips it instead of risking a second training row for the same turn.
487
+ const legacy = sameSession.filter(existing => existing.captureMethod !== 'session-backfill' &&
488
+ (!turnId || !String(existing.writer && existing.writer.turnId || '')));
489
+ if (!legacy.length) return null;
490
+ const sameTurn = existing => Number.isInteger(existing.turn) && existing.turn === record.turn;
491
+ const sameText = existing => Boolean(existing.contentSha256 && existing.contentSha256 === record.contentSha256);
492
+ if (legacy.some(existing => sameTurn(existing) && sameText(existing))) return 'legacy-same-turn';
493
+ if (legacy.some(existing => sameTurn(existing) || sameText(existing))) return 'ambiguous-legacy-turn';
494
+ return null;
495
+ }
496
+
497
+ function findObservationTurnMatch(root, record) {
498
+ const index = readJson(path.join(root, 'observations-index.json'), null);
499
+ return observationTurnMatch(index && index.records, record);
500
+ }
501
+
502
+ function markSessionBackfillBehaviorUnobserved(root, options = {}) {
503
+ const file = path.join(root, 'observations-index.json');
504
+ if (!fs.existsSync(file)) return 0;
505
+ let changed = 0;
506
+ const locked = withLock(root, () => {
507
+ const index = readJson(file, null);
508
+ if (!index || !Array.isArray(index.records)) return;
509
+ for (const record of index.records) {
510
+ if (record.captureMethod === 'session-backfill' && record.behavior !== null) {
511
+ record.behavior = null;
512
+ changed++;
513
+ }
514
+ }
515
+ if (changed) writeJsonAtomic(file, index);
516
+ }, { timeoutMs: options.lockTimeoutMs });
517
+ return locked ? changed : null;
447
518
  }
448
519
 
449
520
  // Tool events are immutable files, one per event, so parallel hooks never
@@ -484,6 +555,7 @@ function startTurn(payload, root) {
484
555
  session.turn++;
485
556
  session.pending = {
486
557
  turn: session.turn,
558
+ turnId: String(payload.turn_id || payload.turnId || ''),
487
559
  startedAt: new Date().toISOString(),
488
560
  prompt: redact(typeof payload.prompt === 'string' ? payload.prompt : ''),
489
561
  previous: session.lastFinal || '',
@@ -505,6 +577,7 @@ function toolTurn(payload, root, failed, codex) {
505
577
 
506
578
  function finishPending(session, payload, root, final) {
507
579
  const pending = session.pending;
580
+ const turnId = String(payload.turn_id || payload.turnId || pending.turnId || '');
508
581
  const { label, reason } = final.text === null && final.reason ? { label: null, reason: final.reason } : parseLabelLine(final.text);
509
582
  const collected = collectEvents(root, payload.session_id, pending.turn);
510
583
  const behavior = { ...collected };
@@ -531,7 +604,10 @@ function finishPending(session, payload, root, final) {
531
604
  contentSha256,
532
605
  host: detectHost(payload, { codex: final.codex || collected._codex }),
533
606
  turn: pending.turn,
534
- writer: { sessionId: String(payload.session_id || 'unknown') },
607
+ writer: {
608
+ sessionId: String(payload.session_id || 'unknown'),
609
+ ...(turnId ? { turnId } : {}),
610
+ },
535
611
  scope: { taskTerms: normalizeTerms(pending.prompt), requirementTerms: [], roles: [] },
536
612
  promptBytes: Buffer.byteLength(pending.prompt, 'utf8'),
537
613
  router: routerState(payload),
@@ -575,7 +651,7 @@ function handle(payload, env = process.env) {
575
651
  }
576
652
 
577
653
  module.exports = {
578
- INTENTS, TIERS, STRATEGIES, enabled, storeRoot, redact, parseLabelLine, classifyCommand, labelTier, contradictions,
579
- normalizeTerms, transcriptMessage, recordTool, emptyBehavior, mergeBehavior, toolFailed, handle, readJson,
654
+ INTENTS, TIERS, STRATEGIES, RETENTION_MS, PREVIOUS_BYTES, enabled, storeRoot, redact, parseLabelLine, stripLabels, tailBytes, sha, classifyCommand, labelTier, contradictions,
655
+ normalizeTerms, transcriptMessage, recordTool, emptyBehavior, mergeBehavior, toolFailed, handle, readJson, appendRecord, appendRecordDetailed, observationTurnMatch, findObservationTurnMatch, markSessionBackfillBehaviorUnobserved,
580
656
  expired, recordText, sweep,
581
657
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "harness-enforcement",
3
3
  "description": "Repository inventory for the opencode enforcement plugin and its standalone helper",
4
- "version": "0.30.9",
4
+ "version": "0.30.10",
5
5
  "runtime": "index.mjs",
6
6
  "hooks": {
7
7
  "verify": "hooks/verify.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.30.9",
3
+ "version": "0.30.10",
4
4
  "description": "Harness OS: An Orchestrated Agent Operating System for AI-driven Development",
5
5
  "main": "harness-everything/scripts/bootstrap.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.30.9",
3
+ "version": "0.30.10",
4
4
  "description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
5
5
  "author": {
6
6
  "name": "Miya Daniel",
@@ -370,15 +370,16 @@ function routerState(payload) {
370
370
  }
371
371
  }
372
372
 
373
- function withLock(root, fn) {
373
+ function withLock(root, fn, options = {}) {
374
374
  fs.mkdirSync(root, { recursive: true, mode: 0o700 });
375
375
  const lock = path.join(root, '.index.lock');
376
- const deadline = Date.now() + 2000;
376
+ const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs >= 0 ? options.timeoutMs : 2000;
377
+ const deadline = Date.now() + timeoutMs;
377
378
  let fd = null;
378
379
  while (fd === null) {
379
380
  try { fd = fs.openSync(lock, 'wx'); } catch (_) {
380
381
  try { if (Date.now() - fs.statSync(lock).mtimeMs > 5000) fs.unlinkSync(lock); } catch (__) { /* raced */ }
381
- if (Date.now() > deadline) return false;
382
+ if (Date.now() >= deadline) return false;
382
383
  const until = Date.now() + 10;
383
384
  while (Date.now() < until) { /* short spin; hooks are short-lived */ }
384
385
  }
@@ -432,18 +433,88 @@ function sweep(root, now = Date.now()) {
432
433
  });
433
434
  }
434
435
 
435
- function appendRecord(root, record, text) {
436
- return withLock(root, () => {
436
+ function appendRecordDetailed(root, record, text, options = {}) {
437
+ let outcome = null;
438
+ const locked = withLock(root, () => {
439
+ const file = path.join(root, 'observations-index.json');
440
+ const index = readJson(file, null) || { schemaVersion: 1, kind: 'system-one-observations', records: [] };
441
+ if (options.dedupeById && index.records.some(existing => existing.id === record.id)) {
442
+ outcome = 'duplicate';
443
+ return;
444
+ }
445
+ if (options.dedupeByTurn) {
446
+ const match = observationTurnMatch(index.records, record);
447
+ if (match) {
448
+ outcome = match === 'ambiguous-legacy-turn' ? 'ambiguous' : 'duplicate';
449
+ return;
450
+ }
451
+ }
437
452
  // The blob is written under the lock, so a concurrent sweep cannot remove it before its record lands.
438
453
  const textDir = path.join(root, 'text');
439
454
  fs.mkdirSync(textDir, { recursive: true, mode: 0o700 });
440
- if (text) fs.writeFileSync(path.join(textDir, `${record.contentSha256}.json`), JSON.stringify(text), { mode: 0o600 });
441
- const file = path.join(root, 'observations-index.json');
442
- const index = readJson(file, null) || { schemaVersion: 1, kind: 'system-one-observations', records: [] };
455
+ if (text) fs.writeFileSync(path.join(textDir, record.contentSha256 + '.json'), JSON.stringify(text), { mode: 0o600 });
443
456
  index.records.push(record);
444
457
  sweepLocked(root, index, Date.now());
445
458
  writeJsonAtomic(file, index);
446
- });
459
+ outcome = 'appended';
460
+ }, { timeoutMs: options.lockTimeoutMs });
461
+ if (!locked) return { status: 'lock-timeout' };
462
+ return { status: outcome || 'appended' };
463
+ }
464
+
465
+ function appendRecord(root, record, text, options = {}) {
466
+ return appendRecordDetailed(root, record, text, options).status === 'appended';
467
+ }
468
+
469
+ function observationTurnMatch(records, record) {
470
+ const writer = record && record.writer || {};
471
+ const sessionId = String(writer.sessionId || '');
472
+ const turnId = String(writer.turnId || '');
473
+ if (!sessionId || !record.host) return null;
474
+ const sameSession = (Array.isArray(records) ? records : []).filter(existing =>
475
+ existing && existing.host === record.host &&
476
+ String(existing.writer && existing.writer.sessionId || '') === sessionId
477
+ );
478
+ if (!sameSession.length) return null;
479
+
480
+ if (turnId && sameSession.some(existing => String(existing.writer && existing.writer.turnId || '') === turnId)) {
481
+ return 'same-turn-id';
482
+ }
483
+
484
+ // Older live records have no turn ID. Only treat matching ordinal plus text
485
+ // as a definite duplicate; a partial match is ambiguous and the importer
486
+ // skips it instead of risking a second training row for the same turn.
487
+ const legacy = sameSession.filter(existing => existing.captureMethod !== 'session-backfill' &&
488
+ (!turnId || !String(existing.writer && existing.writer.turnId || '')));
489
+ if (!legacy.length) return null;
490
+ const sameTurn = existing => Number.isInteger(existing.turn) && existing.turn === record.turn;
491
+ const sameText = existing => Boolean(existing.contentSha256 && existing.contentSha256 === record.contentSha256);
492
+ if (legacy.some(existing => sameTurn(existing) && sameText(existing))) return 'legacy-same-turn';
493
+ if (legacy.some(existing => sameTurn(existing) || sameText(existing))) return 'ambiguous-legacy-turn';
494
+ return null;
495
+ }
496
+
497
+ function findObservationTurnMatch(root, record) {
498
+ const index = readJson(path.join(root, 'observations-index.json'), null);
499
+ return observationTurnMatch(index && index.records, record);
500
+ }
501
+
502
+ function markSessionBackfillBehaviorUnobserved(root, options = {}) {
503
+ const file = path.join(root, 'observations-index.json');
504
+ if (!fs.existsSync(file)) return 0;
505
+ let changed = 0;
506
+ const locked = withLock(root, () => {
507
+ const index = readJson(file, null);
508
+ if (!index || !Array.isArray(index.records)) return;
509
+ for (const record of index.records) {
510
+ if (record.captureMethod === 'session-backfill' && record.behavior !== null) {
511
+ record.behavior = null;
512
+ changed++;
513
+ }
514
+ }
515
+ if (changed) writeJsonAtomic(file, index);
516
+ }, { timeoutMs: options.lockTimeoutMs });
517
+ return locked ? changed : null;
447
518
  }
448
519
 
449
520
  // Tool events are immutable files, one per event, so parallel hooks never
@@ -484,6 +555,7 @@ function startTurn(payload, root) {
484
555
  session.turn++;
485
556
  session.pending = {
486
557
  turn: session.turn,
558
+ turnId: String(payload.turn_id || payload.turnId || ''),
487
559
  startedAt: new Date().toISOString(),
488
560
  prompt: redact(typeof payload.prompt === 'string' ? payload.prompt : ''),
489
561
  previous: session.lastFinal || '',
@@ -505,6 +577,7 @@ function toolTurn(payload, root, failed, codex) {
505
577
 
506
578
  function finishPending(session, payload, root, final) {
507
579
  const pending = session.pending;
580
+ const turnId = String(payload.turn_id || payload.turnId || pending.turnId || '');
508
581
  const { label, reason } = final.text === null && final.reason ? { label: null, reason: final.reason } : parseLabelLine(final.text);
509
582
  const collected = collectEvents(root, payload.session_id, pending.turn);
510
583
  const behavior = { ...collected };
@@ -531,7 +604,10 @@ function finishPending(session, payload, root, final) {
531
604
  contentSha256,
532
605
  host: detectHost(payload, { codex: final.codex || collected._codex }),
533
606
  turn: pending.turn,
534
- writer: { sessionId: String(payload.session_id || 'unknown') },
607
+ writer: {
608
+ sessionId: String(payload.session_id || 'unknown'),
609
+ ...(turnId ? { turnId } : {}),
610
+ },
535
611
  scope: { taskTerms: normalizeTerms(pending.prompt), requirementTerms: [], roles: [] },
536
612
  promptBytes: Buffer.byteLength(pending.prompt, 'utf8'),
537
613
  router: routerState(payload),
@@ -575,7 +651,7 @@ function handle(payload, env = process.env) {
575
651
  }
576
652
 
577
653
  module.exports = {
578
- INTENTS, TIERS, STRATEGIES, enabled, storeRoot, redact, parseLabelLine, classifyCommand, labelTier, contradictions,
579
- normalizeTerms, transcriptMessage, recordTool, emptyBehavior, mergeBehavior, toolFailed, handle, readJson,
654
+ INTENTS, TIERS, STRATEGIES, RETENTION_MS, PREVIOUS_BYTES, enabled, storeRoot, redact, parseLabelLine, stripLabels, tailBytes, sha, classifyCommand, labelTier, contradictions,
655
+ normalizeTerms, transcriptMessage, recordTool, emptyBehavior, mergeBehavior, toolFailed, handle, readJson, appendRecord, appendRecordDetailed, observationTurnMatch, findObservationTurnMatch, markSessionBackfillBehaviorUnobserved,
580
656
  expired, recordText, sweep,
581
657
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "harness-everything",
4
- "version": "0.30.9",
4
+ "version": "0.30.10",
5
5
  "description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
6
6
  "author": {
7
7
  "name": "Miya Daniel",
package/sbom.cdx.json CHANGED
@@ -2,23 +2,23 @@
2
2
  "$schema": "https://cyclonedx.org/schema/bom-1.7.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.7",
5
- "serialNumber": "urn:uuid:fdd6adf4-625c-5667-8d1c-1dd030c7b4a4",
5
+ "serialNumber": "urn:uuid:f44be0e3-9e57-5a51-906c-29b1711d3951",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-10-08T03:48:56.742Z",
8
+ "timestamp": "2026-10-13T05:15:41.988Z",
9
9
  "tools": [
10
10
  {
11
11
  "vendor": "Harness Everything",
12
12
  "name": "generate-sbom.js",
13
- "version": "0.30.9"
13
+ "version": "0.30.10"
14
14
  }
15
15
  ],
16
16
  "component": {
17
17
  "type": "application",
18
- "bom-ref": "pkg:npm/harness-everything@0.30.9",
18
+ "bom-ref": "pkg:npm/harness-everything@0.30.10",
19
19
  "name": "harness-everything",
20
- "version": "0.30.9",
21
- "purl": "pkg:npm/harness-everything@0.30.9"
20
+ "version": "0.30.10",
21
+ "purl": "pkg:npm/harness-everything@0.30.10"
22
22
  }
23
23
  },
24
24
  "components": [
@@ -1999,7 +1999,7 @@
1999
1999
  ]
2000
2000
  },
2001
2001
  {
2002
- "ref": "pkg:npm/harness-everything@0.30.9",
2002
+ "ref": "pkg:npm/harness-everything@0.30.10",
2003
2003
  "dependsOn": [
2004
2004
  "pkg:npm/%40commitlint/cli@19.8.1",
2005
2005
  "pkg:npm/%40commitlint/config-conventional@19.8.1",
@@ -52,10 +52,23 @@ function defaultCodexHome(env = process.env) {
52
52
  return path.resolve(env.CODEX_HOME || path.join(os.homedir(), '.codex'));
53
53
  }
54
54
 
55
- function pathsFor(codexHome, sourceHash = null) {
55
+ function sourcePluginVersion(sourcePluginRoot) {
56
+ for (const manifestFile of [
57
+ path.join(sourcePluginRoot, '.codex-plugin', 'plugin.json'),
58
+ path.join(sourcePluginRoot, 'plugin.json')
59
+ ]) {
60
+ const manifest = readJsonObject(manifestFile, null);
61
+ if (manifest && typeof manifest.version === 'string' && /^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$/.test(manifest.version)) {
62
+ return manifest.version;
63
+ }
64
+ }
65
+ throw new Error('installed plugin source is missing a valid plugin version: ' + sourcePluginRoot);
66
+ }
67
+
68
+ function pathsFor(codexHome, sourceHash = null, packageVersion = PACKAGE_VERSION) {
56
69
  const home = path.resolve(codexHome);
57
70
  const ownerRoot = path.join(home, 'harness-everything', 'compat-hooks');
58
- const runtimeId = sourceHash ? PACKAGE_VERSION + '-' + sourceHash.slice(0, 12) : null;
71
+ const runtimeId = sourceHash ? packageVersion + '-' + sourceHash.slice(0, 12) : null;
59
72
  return {
60
73
  codexHome: home,
61
74
  hooksFile: path.join(home, 'hooks.json'),
@@ -290,11 +303,11 @@ function hookEntriesFrom(pluginRoot, runtimeDir) {
290
303
  return records;
291
304
  }
292
305
 
293
- function sourceHookEntries(runtimeDir) {
294
- return hookEntriesFrom(SOURCE_PLUGIN_ROOT, runtimeDir);
306
+ function sourceHookEntries(runtimeDir, sourcePluginRoot = SOURCE_PLUGIN_ROOT) {
307
+ return hookEntriesFrom(sourcePluginRoot, runtimeDir);
295
308
  }
296
309
 
297
- function ensureRuntime(runtimeDir, sourceHash) {
310
+ function ensureRuntime(runtimeDir, sourceHash, sourcePluginRoot = SOURCE_PLUGIN_ROOT) {
298
311
  if (fs.existsSync(runtimeDir)) {
299
312
  if (treeHash(runtimeDir) === sourceHash) return;
300
313
  fs.rmSync(runtimeDir, { recursive: true, force: true });
@@ -303,7 +316,7 @@ function ensureRuntime(runtimeDir, sourceHash) {
303
316
  const temp = runtimeDir + '.tmp-' + process.pid;
304
317
  try {
305
318
  fs.rmSync(temp, { recursive: true, force: true });
306
- fs.cpSync(SOURCE_PLUGIN_ROOT, temp, { recursive: true });
319
+ fs.cpSync(sourcePluginRoot, temp, { recursive: true });
307
320
  if (treeHash(temp) !== sourceHash) throw new Error('copied compatibility runtime hash mismatch');
308
321
  fs.renameSync(temp, runtimeDir);
309
322
  } finally {
@@ -313,8 +326,10 @@ function ensureRuntime(runtimeDir, sourceHash) {
313
326
 
314
327
  function install(options = {}) {
315
328
  const codexHome = path.resolve(options.codexHome || defaultCodexHome(options.env));
316
- const sourceHash = treeHash(SOURCE_PLUGIN_ROOT);
317
- const p = pathsFor(codexHome, sourceHash);
329
+ const sourcePluginRoot = path.resolve(options.sourcePluginRoot || SOURCE_PLUGIN_ROOT);
330
+ const packageVersion = sourcePluginVersion(sourcePluginRoot);
331
+ const sourceHash = treeHash(sourcePluginRoot);
332
+ const p = pathsFor(codexHome, sourceHash, packageVersion);
318
333
  assertCompatibilityPaths(p);
319
334
  const existedBefore = fs.existsSync(p.hooksFile);
320
335
  const hooksBefore = existedBefore ? fs.readFileSync(p.hooksFile) : null;
@@ -331,9 +346,9 @@ function install(options = {}) {
331
346
  base = removal.config;
332
347
  }
333
348
 
334
- ensureRuntime(p.runtimeDir, sourceHash);
349
+ ensureRuntime(p.runtimeDir, sourceHash, sourcePluginRoot);
335
350
  assertCompatibilityPaths(p);
336
- const generated = sourceHookEntries(p.runtimeDir);
351
+ const generated = sourceHookEntries(p.runtimeDir, sourcePluginRoot);
337
352
  const ownedEntries = [];
338
353
  const next = { ...base, hooks: { ...base.hooks } };
339
354
  for (const record of generated) {
@@ -349,7 +364,7 @@ function install(options = {}) {
349
364
  schemaVersion: MANIFEST_SCHEMA,
350
365
  package: 'harness-everything',
351
366
  mode: MANIFEST_MODE,
352
- packageVersion: PACKAGE_VERSION,
367
+ packageVersion,
353
368
  sourcePluginSha256: sourceHash,
354
369
  runtimeDir: p.runtimeDir,
355
370
  hooksFile: p.hooksFile,
@@ -385,6 +400,8 @@ function install(options = {}) {
385
400
  codexHome,
386
401
  hooksFile: p.hooksFile,
387
402
  runtimeDir: p.runtimeDir,
403
+ packageVersion,
404
+ sourcePluginSha256: sourceHash,
388
405
  entries: generated.length,
389
406
  trustChanged: false,
390
407
  };
@@ -443,10 +460,13 @@ function status(options = {}) {
443
460
  const p = pathsFor(codexHome);
444
461
  assertCompatibilityPaths(p);
445
462
  const prior = readManifest(p.manifestFile, p);
446
- if (!prior) return { installed: false, codexHome, hooksFile: p.hooksFile, trustChanged: false };
463
+ if (!prior) return { installed: false, codexHome, hooksFile: p.hooksFile, trustChanged: false,
464
+ observationCapture: { status: 'inactive', reason: 'compatibility-hooks-not-installed', trustRequired: true } };
447
465
  const config = readHooksFile(p.hooksFile);
448
466
  const removal = removePriorEntries(config, prior);
449
467
  const expected = prior.entries.length;
468
+ const healthy = removal.removed.length === expected && removal.drift.length === 0 &&
469
+ fs.existsSync(prior.runtimeDir) && treeHash(prior.runtimeDir) === prior.sourcePluginSha256;
450
470
  return {
451
471
  installed: true,
452
472
  codexHome,
@@ -457,8 +477,12 @@ function status(options = {}) {
457
477
  expectedEntries: expected,
458
478
  exactEntriesPresent: removal.removed.length,
459
479
  drift: removal.drift,
460
- healthy: removal.removed.length === expected && removal.drift.length === 0 &&
461
- fs.existsSync(prior.runtimeDir) && treeHash(prior.runtimeDir) === prior.sourcePluginSha256,
480
+ healthy,
481
+ observationCapture: {
482
+ status: healthy ? 'configured-unverified' : 'inactive',
483
+ reason: healthy ? 'codex-hook-trust-state-is-not-readable-here' : 'compatibility-hooks-unhealthy',
484
+ trustRequired: true,
485
+ },
462
486
  trustChanged: false,
463
487
  };
464
488
  }
@@ -511,6 +535,7 @@ module.exports = {
511
535
  main,
512
536
  pathsFor,
513
537
  readHooksFile,
538
+ sourcePluginVersion,
514
539
  removePriorEntries,
515
540
  sourceHookEntries,
516
541
  stableJson,