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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +14 -0
- package/benchmarks/results/live-host/codex-2026-10-13/README.md +55 -0
- package/benchmarks/results/live-host/codex-2026-10-13/observation-records.json +88 -0
- package/ci/doc-capability-consistency.test.js +11 -1
- package/ci/mechanism-2al-codex-hooks-compat.test.js +19 -0
- package/ci/mechanism-2am-observations-session-import.test.js +221 -0
- package/ci/mechanism-2ao-plugin-sync.test.js +129 -4
- package/ci/mechanism-2w-platform-compatibility.test.js +12 -0
- package/ci/mechanism-34-observations.test.js +64 -0
- package/docs/platform-capabilities.md +8 -8
- package/docs/platform-compatibility.json +5 -5
- package/docs/system-one-observations.md +35 -8
- package/hooks/scripts/lib/observations.js +88 -12
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/lib/observations.js +88 -12
- package/plugins/harness-everything/plugin.json +1 -1
- package/sbom.cdx.json +7 -7
- package/scripts/codex-hooks-compat.js +39 -14
- package/scripts/plugin-sync.js +100 -29
- package/scripts/system-one-observations-export.js +29 -2
- package/scripts/system-one-observations-import-codex-sessions.js +307 -0
- package/scripts/system-one-observations-review.js +11 -1
|
@@ -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
|
|
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()
|
|
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
|
|
436
|
-
|
|
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,
|
|
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: {
|
|
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
|
};
|
package/package.json
CHANGED
|
@@ -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
|
|
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()
|
|
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
|
|
436
|
-
|
|
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,
|
|
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: {
|
|
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.
|
|
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:
|
|
5
|
+
"serialNumber": "urn:uuid:f44be0e3-9e57-5a51-906c-29b1711d3951",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-10-
|
|
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.
|
|
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.
|
|
18
|
+
"bom-ref": "pkg:npm/harness-everything@0.30.10",
|
|
19
19
|
"name": "harness-everything",
|
|
20
|
-
"version": "0.30.
|
|
21
|
-
"purl": "pkg:npm/harness-everything@0.30.
|
|
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.
|
|
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
|
|
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 ?
|
|
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(
|
|
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(
|
|
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
|
|
317
|
-
const
|
|
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
|
|
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
|
|
461
|
-
|
|
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,
|