harness-everything 0.30.8 → 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/.github/workflows/ci.yml +31 -0
- package/CHANGELOG.md +21 -0
- package/README.md +1 -1
- 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/fixtures/system-one-resident-stub.py +7 -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/ci/mechanism-system-one-resident.test.js +70 -3
- 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
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "harness-everything",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
|
|
13
|
-
"version": "0.30.
|
|
13
|
+
"version": "0.30.10",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Miya Daniel"
|
|
16
16
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harness-everything",
|
|
3
3
|
"description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
|
|
4
|
-
"version": "0.30.
|
|
4
|
+
"version": "0.30.10",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Miya Daniel"
|
|
7
7
|
},
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -29,6 +29,37 @@ jobs:
|
|
|
29
29
|
- name: Run focused mutation checks
|
|
30
30
|
run: npm run test:mutations
|
|
31
31
|
|
|
32
|
+
# Pin the patched version: floating Node 24 already passed before #284.
|
|
33
|
+
system-one-windows-node24-patched:
|
|
34
|
+
runs-on: windows-latest
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v7
|
|
37
|
+
- uses: actions/setup-node@v7
|
|
38
|
+
with:
|
|
39
|
+
node-version: '24.20.0'
|
|
40
|
+
- name: Verify exact patched runtime
|
|
41
|
+
run: node -e "if (process.versions.node !== '24.20.0') process.exit(1)"
|
|
42
|
+
- name: Exercise real evaluator subprocess and repeated resident teardown
|
|
43
|
+
run: node --test ci/mechanism-system-one-resident.test.js
|
|
44
|
+
|
|
45
|
+
# Node >=22 also includes the odd-numbered Node 23 line. Keep its
|
|
46
|
+
# documented Windows shutdown failure visible, without narrowing engines.
|
|
47
|
+
system-one-windows-node-legacy-policy:
|
|
48
|
+
runs-on: windows-latest
|
|
49
|
+
strategy:
|
|
50
|
+
fail-fast: false
|
|
51
|
+
matrix:
|
|
52
|
+
node: ['23.6.0', '24.15.0']
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@v7
|
|
55
|
+
- uses: actions/setup-node@v7
|
|
56
|
+
with:
|
|
57
|
+
node-version: ${{ matrix.node }}
|
|
58
|
+
- name: Verify exact affected runtime
|
|
59
|
+
run: node -e "if (process.versions.node !== '${{ matrix.node }}') process.exit(1)"
|
|
60
|
+
- name: Confirm explicit RS13 skip with version-policy unit coverage
|
|
61
|
+
run: node --test --test-name-pattern="S1-RS00|S1-RS13" ci/mechanism-system-one-resident.test.js
|
|
62
|
+
|
|
32
63
|
runtime-floor:
|
|
33
64
|
runs-on: ubuntu-latest
|
|
34
65
|
steps:
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.30.10](https://github.com/dyphn1/Harness-everything/compare/v0.30.9...v0.30.10) (2026-10-13)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
* **codex:** address observation review findings ([b869cfb](https://github.com/dyphn1/Harness-everything/commit/b869cfbdb44172ee88ad6ae3f76e29c8da6e9da8))
|
|
10
|
+
* **codex:** sync hooks and recover turn labels ([d29112e](https://github.com/dyphn1/Harness-everything/commit/d29112e15ac255c5ded6fcf2aab3a21c710b3def)), closes [#304](https://github.com/dyphn1/Harness-everything/issues/304)
|
|
11
|
+
* **codex:** validate sources and report import failures ([3f57b8b](https://github.com/dyphn1/Harness-everything/commit/3f57b8b376bfd437c8cff6ddbe4ddbf036380198)), closes [#304](https://github.com/dyphn1/Harness-everything/issues/304)
|
|
12
|
+
|
|
13
|
+
## [Unreleased]
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **codex:** keep compatibility hooks and session labels in sync
|
|
18
|
+
|
|
19
|
+
## [0.30.9](https://github.com/dyphn1/Harness-everything/compare/v0.30.8...v0.30.9) (2026-10-08)
|
|
20
|
+
|
|
21
|
+
### Bug Fixes
|
|
22
|
+
|
|
23
|
+
* **system-one:** cover Windows Node 23 shutdown regression ([75ef983](https://github.com/dyphn1/Harness-everything/commit/75ef9833e4fcfb697c8323836f08eed3dfc7ff8d))
|
|
24
|
+
* **test:** correct Windows Node version policy regex ([c0b63cc](https://github.com/dyphn1/Harness-everything/commit/c0b63cc3f18dc7126e4680f4fa7682ea2a4729ef))
|
|
25
|
+
|
|
5
26
|
## [0.30.8](https://github.com/dyphn1/Harness-everything/compare/v0.30.7...v0.30.8) (2026-10-07)
|
|
6
27
|
|
|
7
28
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ The exact "Harness" behavior depends on the installation surface. Claude Code ha
|
|
|
48
48
|
|
|
49
49
|
Harness integrates directly into your workspace. There is no heavy daemon, no paid external APIs, and zero configuration required.
|
|
50
50
|
|
|
51
|
-
**Runtime:** Harness supports **Node.js 22
|
|
51
|
+
**Runtime:** Harness supports **Node.js 22+** (including odd-numbered versions); Node.js **24** is the primary development and CI runtime (`.nvmrc`). **On Windows, System One evaluator subprocess verification should use Node 22 or Node 24.20+**. Node 23.x has a documented Windows process-shutdown assertion ([nodejs/node#56645](https://github.com/nodejs/node/issues/56645)); Node 24.0–24.19 predates the backported libuv fix ([nodejs/node#61999](https://github.com/nodejs/node/pull/61999)) and may likewise crash at process exit (`0xC0000409`). RS13 visibly skips **only** on those Windows versions while retaining the real evaluator subprocess elsewhere. A skip is not a pass or evidence of a fixed Node runtime. CI pins Windows Node 23.6.0 and 24.15.0 to validate their skip policy, and Node 24.20.0 to run the complete subprocess/regression suite. These tests do not reduce the package's general `>=22` engine compatibility claim. The generated current-state runtime/workflow summary is [docs/repository-contract.md](docs/repository-contract.md).
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
54
|
# Option A: Claude Code plugin (marketplace manifest included)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Live-host evidence: Codex CLI observation capture
|
|
2
|
+
|
|
3
|
+
This records live dispatch of the Codex user-hook compatibility fallback on one Windows host. It is mechanism/live-host evidence for turn capture, not a behavioral-effectiveness benchmark and not evidence for native plugin-hook mounting.
|
|
4
|
+
|
|
5
|
+
- Date: 2026-10-13 UTC
|
|
6
|
+
- Host: Codex CLI 0.162.1; Windows `10.0.26200` x64; Node.js v24.18.0
|
|
7
|
+
- Repository revision under test: PR #305, `3f57b8b376bfd437c8cff6ddbe4ddbf036380198`
|
|
8
|
+
- Packaged compatibility runtime: version 0.30.9; SHA-256 `7f4e7801d9fdb7350072b01c91034cd28890dfe7190d143ec7760c85fd5a29df`, matching `plugins/harness-everything/`
|
|
9
|
+
- Compatibility status: files healthy; status command reports `configured-unverified` because it cannot read Codex trust state.
|
|
10
|
+
|
|
11
|
+
## Procedure
|
|
12
|
+
|
|
13
|
+
The Codex `/hooks` UI showed the observation handlers as new/untrusted. I reviewed and trusted only the `UserPromptSubmit` and `Stop` handlers named `Recording a local System One turn observation`. I did not trust all hooks. No trust bypass flag was used. The successful probes ran through `codex exec` with read-only sandboxing and no tool calls. A second turn resumed the same session.
|
|
14
|
+
|
|
15
|
+
Two preliminary attempts were not counted as passing evidence: before trust, a completed CLI turn produced no observation; a later argument-passed prompt lost JSON quotes in Windows `.cmd` argument handling and created an invalid-label record. The accepted run passed its prompt through stdin, preserving the valid JSON label. The probe prompt and raw rollout stay in the private local Codex/observation stores and are not committed here.
|
|
16
|
+
|
|
17
|
+
## Results
|
|
18
|
+
|
|
19
|
+
The sanitized snapshot is [observation-records.json](observation-records.json). Both records have `host: "codex"`, the same session ID, sequential `turn` values, distinct `turn_id` values, and the self-report expected for their own turn:
|
|
20
|
+
|
|
21
|
+
| Turn | Record ID | Turn ID | Self-report intent | Result |
|
|
22
|
+
|---:|---|---|---|---|
|
|
23
|
+
| 1 | `09ba6878d736e3f7183e7c3a` | `01a1338c-9727-7743-824d-92f8ceb51f00` | `test` | Passed |
|
|
24
|
+
| 2 | `81a4c3a35f7894653aaf9f64` | `01a1338f-6ea2-7671-997f-e9a205b96102` | `discuss` | Passed |
|
|
25
|
+
|
|
26
|
+
The second turn did not reuse the first turn's label. Prompt text, prompt hashes, and raw session content are omitted from this committed artifact. The live records showed zero tool calls, as expected for these reply-only probes; this run does not verify tool-event counting.
|
|
27
|
+
|
|
28
|
+
## Limits
|
|
29
|
+
|
|
30
|
+
- Verified scope: one Windows Codex CLI 0.162.1 setup using the user-hook compatibility fallback.
|
|
31
|
+
- Not verified: native plugin-bundled hook mounting, marketplace source reporting during `plugin-sync`, other Codex versions/platforms, other lifecycle events, or behavioral effectiveness.
|
|
32
|
+
- The CLI emitted a non-fatal GitHub MCP `AuthRequired` diagnostic and a bundled `SessionEnd` timeout-clamping notice. Both turns completed with exit code 0 and their observation records were present.
|
|
33
|
+
|
|
34
|
+
## Repository verification
|
|
35
|
+
|
|
36
|
+
All repository gates listed in `AGENTS.md` passed on this PR revision. The four gates affected by the new Codex evidence (`npm test`, `test:mechanism`, `test:consistency`, and `test:docs:capabilities`) initially exposed stale assertions that required Codex live evidence to remain `Unknown`; the assertions were updated to validate the retained artifact, then those gates passed on rerun.
|
|
37
|
+
|
|
38
|
+
| Gate | Result |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `npm test` | Passed on rerun; 2 environment-dependent cases skipped |
|
|
41
|
+
| `npm run test:mutations` | Passed |
|
|
42
|
+
| `npm run test:mechanism` | Passed on rerun |
|
|
43
|
+
| `npm run test:consistency` | Passed on rerun |
|
|
44
|
+
| `npm run test:repo-contract` | Passed |
|
|
45
|
+
| `npm run test:docs:capabilities` | Passed on rerun |
|
|
46
|
+
| `npm run test:references` | Passed |
|
|
47
|
+
| `npm run test:release` | Passed |
|
|
48
|
+
| `npm run test:collision` | Passed |
|
|
49
|
+
| `npm run test:routing:skills` | Passed |
|
|
50
|
+
| `npm run test:routing:invariants` | Passed |
|
|
51
|
+
| `npm run test:plugin:openai` | Passed |
|
|
52
|
+
| `npm run test:plugin:submission` | Passed |
|
|
53
|
+
| `npm run test:platform:compatibility` | Passed on rerun |
|
|
54
|
+
|
|
55
|
+
The gates were invoked through `npm.cmd` in PowerShell because this host blocks the `npm.ps1` shim by execution policy. Waza was not installed, so optional Waza checks were unavailable.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "harness-codex-live-observation-evidence-v1",
|
|
3
|
+
"testedAt": "2026-10-13",
|
|
4
|
+
"source": {
|
|
5
|
+
"pr": 305,
|
|
6
|
+
"commit": "3f57b8b376bfd437c8cff6ddbe4ddbf036380198",
|
|
7
|
+
"runtimeVersion": "0.30.9",
|
|
8
|
+
"runtimeSha256": "7f4e7801d9fdb7350072b01c91034cd28890dfe7190d143ec7760c85fd5a29df"
|
|
9
|
+
},
|
|
10
|
+
"host": {
|
|
11
|
+
"product": "Codex CLI",
|
|
12
|
+
"version": "0.162.1",
|
|
13
|
+
"os": "win32 10.0.26200 x64",
|
|
14
|
+
"node": "v24.18.0"
|
|
15
|
+
},
|
|
16
|
+
"capture": {
|
|
17
|
+
"path": "user-hook compatibility fallback",
|
|
18
|
+
"trustedHandlers": [
|
|
19
|
+
"UserPromptSubmit: Recording a local System One turn observation",
|
|
20
|
+
"Stop: Recording a local System One turn observation"
|
|
21
|
+
],
|
|
22
|
+
"trustBypass": false,
|
|
23
|
+
"rawPromptCommitted": false,
|
|
24
|
+
"rawSessionCommitted": false
|
|
25
|
+
},
|
|
26
|
+
"sessionId": "01a1338c-914c-7bf0-b893-3b5e538a129d",
|
|
27
|
+
"turns": [
|
|
28
|
+
{
|
|
29
|
+
"recordId": "09ba6878d736e3f7183e7c3a",
|
|
30
|
+
"host": "codex",
|
|
31
|
+
"observedAt": "2026-10-13T04:37:02.348Z",
|
|
32
|
+
"turn": 1,
|
|
33
|
+
"turnId": "01a1338c-9727-7743-824d-92f8ceb51f00",
|
|
34
|
+
"selfReport": {
|
|
35
|
+
"v": 1,
|
|
36
|
+
"validity": "actionable",
|
|
37
|
+
"contextDependent": false,
|
|
38
|
+
"tier": "tier1",
|
|
39
|
+
"intents": [
|
|
40
|
+
"test"
|
|
41
|
+
],
|
|
42
|
+
"workflow": null,
|
|
43
|
+
"skills": []
|
|
44
|
+
},
|
|
45
|
+
"selfReportReason": null,
|
|
46
|
+
"behavior": {
|
|
47
|
+
"toolCalls": 0,
|
|
48
|
+
"toolFailures": 0,
|
|
49
|
+
"filesWritten": 0,
|
|
50
|
+
"filesCreated": 0
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"recordId": "81a4c3a35f7894653aaf9f64",
|
|
55
|
+
"host": "codex",
|
|
56
|
+
"observedAt": "2026-10-13T04:40:07.308Z",
|
|
57
|
+
"turn": 2,
|
|
58
|
+
"turnId": "01a1338f-6ea2-7671-997f-e9a205b96102",
|
|
59
|
+
"selfReport": {
|
|
60
|
+
"v": 1,
|
|
61
|
+
"validity": "actionable",
|
|
62
|
+
"contextDependent": false,
|
|
63
|
+
"tier": "tier1",
|
|
64
|
+
"intents": [
|
|
65
|
+
"discuss"
|
|
66
|
+
],
|
|
67
|
+
"workflow": null,
|
|
68
|
+
"skills": []
|
|
69
|
+
},
|
|
70
|
+
"selfReportReason": null,
|
|
71
|
+
"behavior": {
|
|
72
|
+
"toolCalls": 0,
|
|
73
|
+
"toolFailures": 0,
|
|
74
|
+
"filesWritten": 0,
|
|
75
|
+
"filesCreated": 0
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
"assertions": {
|
|
80
|
+
"oneRecordPerTurn": true,
|
|
81
|
+
"bothHostCodex": true,
|
|
82
|
+
"sameSession": true,
|
|
83
|
+
"sequentialTurns": true,
|
|
84
|
+
"distinctTurnIds": true,
|
|
85
|
+
"currentTurnLabelsMatch": true,
|
|
86
|
+
"promptContentOmitted": true
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -190,6 +190,7 @@ for (const rel of OPENCODE_SURFACES) {
|
|
|
190
190
|
|
|
191
191
|
const matrix = JSON.parse(read('docs/platform-compatibility.json'));
|
|
192
192
|
const opencode = matrix.platforms.find(platform => platform.id === 'opencode');
|
|
193
|
+
const codex = matrix.platforms.find(platform => platform.id === 'codex');
|
|
193
194
|
for (const dimension of ['hooksLifecycle', 'liveHostVerification']) {
|
|
194
195
|
if (opencode?.capabilities[dimension]?.status !== 'Partial') {
|
|
195
196
|
fail(`OpenCode ${dimension}: must remain Partial`);
|
|
@@ -198,7 +199,16 @@ for (const dimension of ['hooksLifecycle', 'liveHostVerification']) {
|
|
|
198
199
|
if (opencode?.capabilities.liveHostVerification.evidence !== OPENCODE_EVIDENCE) {
|
|
199
200
|
fail('OpenCode liveHostVerification: missing exact retained artifact path');
|
|
200
201
|
}
|
|
201
|
-
|
|
202
|
+
if (codex?.capabilities.hooksLifecycle.status !== 'Partial') {
|
|
203
|
+
fail('Codex hooksLifecycle: expected partial live verification for the compatibility fallback');
|
|
204
|
+
}
|
|
205
|
+
if (codex?.capabilities.liveHostVerification.status !== 'Partial') {
|
|
206
|
+
fail('Codex liveHostVerification: expected partial status with preserved evidence');
|
|
207
|
+
}
|
|
208
|
+
if (codex?.capabilities.liveHostVerification.evidence !== 'benchmarks/results/live-host/codex-2026-10-13/README.md') {
|
|
209
|
+
fail('Codex liveHostVerification: missing exact retained artifact path');
|
|
210
|
+
}
|
|
211
|
+
for (const platform of matrix.platforms.filter(platform => !['opencode', 'codex'].includes(platform.id))) {
|
|
202
212
|
if (platform.capabilities.liveHostVerification.status !== 'Unknown') {
|
|
203
213
|
fail(`${platform.id}: liveHostVerification must remain Unknown without its own evidence`);
|
|
204
214
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Stub scorer behind the real resident server code; never model-quality evidence."""
|
|
2
2
|
import importlib.util
|
|
3
|
+
import json
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
import sys
|
|
5
6
|
import time
|
|
@@ -28,4 +29,9 @@ def factory(weights, manifest):
|
|
|
28
29
|
if __name__ == '__main__':
|
|
29
30
|
if sys.argv[1:2] != ['--serve']:
|
|
30
31
|
sys.exit(2)
|
|
31
|
-
|
|
32
|
+
result = adapter.serve_main(sys.argv[2:], scorer_factory=factory)
|
|
33
|
+
# Deterministic teardown fixture: the state/socket are closed, but PID is
|
|
34
|
+
# deliberately alive to distinguish state unlink from full process exit.
|
|
35
|
+
if json.loads(sys.argv[2]).get('modelId') == 'stub-exit-linger':
|
|
36
|
+
time.sleep(0.5)
|
|
37
|
+
sys.exit(result)
|
|
@@ -73,6 +73,8 @@ try {
|
|
|
73
73
|
const state = compat.status({ codexHome });
|
|
74
74
|
check(state.installed && state.healthy && state.exactEntriesPresent === state.expectedEntries,
|
|
75
75
|
'status verifies exact installed entry hashes and runtime tree');
|
|
76
|
+
check(state.observationCapture.status === 'configured-unverified' && state.observationCapture.trustRequired,
|
|
77
|
+
'status distinguishes installed hooks from Codex-trusted live capture');
|
|
76
78
|
|
|
77
79
|
const config = read(hooksFile);
|
|
78
80
|
check(config.description === original.description &&
|
|
@@ -158,6 +160,18 @@ try {
|
|
|
158
160
|
check(!fs.existsSync(path.join(emptyHome, 'hooks.json')),
|
|
159
161
|
'uninstall removes hooks.json only when Harness created it and no user entries remain');
|
|
160
162
|
|
|
163
|
+
const marketplaceSource = path.join(root, 'marketplace-plugin');
|
|
164
|
+
const marketplaceHome = path.join(root, 'marketplace-codex');
|
|
165
|
+
fs.cpSync(compat.SOURCE_PLUGIN_ROOT, marketplaceSource, { recursive: true });
|
|
166
|
+
fs.writeFileSync(path.join(marketplaceSource, 'hooks', 'marketplace-source-marker.txt'), 'newer marketplace source\n');
|
|
167
|
+
const marketplaceInstall = compat.install({ codexHome: marketplaceHome, sourcePluginRoot: marketplaceSource });
|
|
168
|
+
const marketplaceManifest = read(path.join(marketplaceHome, 'harness-everything', 'compat-hooks', 'manifest.json'));
|
|
169
|
+
check(marketplaceInstall.sourcePluginSha256 === compat.treeHash(marketplaceSource) &&
|
|
170
|
+
marketplaceManifest.sourcePluginSha256 === compat.treeHash(marketplaceSource) &&
|
|
171
|
+
fs.readFileSync(path.join(marketplaceManifest.runtimeDir, 'hooks', 'marketplace-source-marker.txt'), 'utf8') === 'newer marketplace source\n',
|
|
172
|
+
'compatibility runtime is copied from the verified installed marketplace source');
|
|
173
|
+
compat.uninstall({ codexHome: marketplaceHome });
|
|
174
|
+
|
|
161
175
|
const duplicateHome = path.join(root, 'duplicate-codex');
|
|
162
176
|
fs.mkdirSync(duplicateHome, { recursive: true });
|
|
163
177
|
compat.install({ codexHome: duplicateHome });
|
|
@@ -273,6 +287,11 @@ try {
|
|
|
273
287
|
check(malformedRefused && fs.readFileSync(malformed, 'utf8') === malformedBefore,
|
|
274
288
|
'malformed user hooks.json fails closed without replacement');
|
|
275
289
|
|
|
290
|
+
const inactive = compat.status({ codexHome: path.join(root, 'not-installed') });
|
|
291
|
+
check(inactive.observationCapture.status === 'inactive' &&
|
|
292
|
+
inactive.observationCapture.reason === 'compatibility-hooks-not-installed',
|
|
293
|
+
'status clearly reports inactive observation capture when hooks are absent');
|
|
294
|
+
|
|
276
295
|
const source = read(path.join(compat.SOURCE_PLUGIN_ROOT, 'hooks', 'hooks.json'));
|
|
277
296
|
const sourceCount = Object.values(source.hooks).reduce((sum, entries) => sum + entries.length, 0);
|
|
278
297
|
check(sourceCount === installed.entries,
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const assert = require('assert');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
11
|
+
const IMPORTER = path.join(ROOT, 'scripts', 'system-one-observations-import-codex-sessions.js');
|
|
12
|
+
const EXPORTER = path.join(ROOT, 'scripts', 'system-one-observations-export.js');
|
|
13
|
+
const REVIEW = path.join(ROOT, 'scripts', 'system-one-observations-review.js');
|
|
14
|
+
const obs = require('../hooks/scripts/lib/observations');
|
|
15
|
+
const LABEL = '<!-- harness-label {"v":1,"validity":"actionable","contextDependent":false,"tier":"tier2","intents":["fix"],"workflow":null,"skills":[]} -->';
|
|
16
|
+
const TIER1_LABEL = '<!-- harness-label {"v":1,"validity":"actionable","contextDependent":false,"tier":"tier1","intents":["fix"],"workflow":null,"skills":[]} -->';
|
|
17
|
+
|
|
18
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-session-label-import-'));
|
|
19
|
+
const sessions = path.join(root, 'sessions');
|
|
20
|
+
const store = path.join(root, 'observations');
|
|
21
|
+
fs.mkdirSync(path.join(sessions, '2026', '10'), { recursive: true });
|
|
22
|
+
const rollout = path.join(sessions, '2026', '10', 'rollout-fixture.jsonl');
|
|
23
|
+
|
|
24
|
+
const rows = [
|
|
25
|
+
{ type: 'session_meta', payload: { session_id: 'fixture-session' } },
|
|
26
|
+
{ type: 'event_msg', timestamp: '2026-09-26T23:59:59.000Z', payload: { type: 'task_started', turn_id: 'old', started_at: '2026-09-26T23:59:59.000Z' } },
|
|
27
|
+
{ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'pre-revision prompt' }] } },
|
|
28
|
+
{ type: 'event_msg', timestamp: '2026-09-26T23:59:59.500Z', payload: { type: 'task_complete', turn_id: 'old', last_agent_message: LABEL } },
|
|
29
|
+
{ type: 'event_msg', timestamp: '2026-09-27T00:00:00.000Z', payload: { type: 'task_started', turn_id: 'included', started_at: '2026-09-27T00:00:00.000Z' } },
|
|
30
|
+
{ type: 'response_item', payload: { type: 'message', role: 'user', content: [
|
|
31
|
+
{ type: 'input_text', text: 'Please fix the parser. Contact user@example.com.' },
|
|
32
|
+
{ type: 'input_text', text: 'injected host context must stay out of the prompt row' }
|
|
33
|
+
] } },
|
|
34
|
+
{ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'later context message' }] } },
|
|
35
|
+
{ type: 'event_msg', timestamp: '2026-09-27T00:01:00.000Z', payload: { type: 'task_complete', turn_id: 'included', last_agent_message: 'Done.\n' + LABEL } },
|
|
36
|
+
{ type: 'event_msg', timestamp: '2026-09-30T00:00:00.000Z', payload: { type: 'task_started', turn_id: 'assistant-fallback', started_at: '2026-09-30T00:00:00.000Z' } },
|
|
37
|
+
{ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'fallback prompt from the rollout message' }] } },
|
|
38
|
+
{ type: 'response_item', payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'Completed answer.\n' + LABEL }] } },
|
|
39
|
+
{ type: 'event_msg', timestamp: '2026-09-30T00:01:00.000Z', payload: { type: 'task_complete', turn_id: 'assistant-fallback' } },
|
|
40
|
+
{ type: 'event_msg', timestamp: '2026-09-30T00:00:00.000Z', payload: { type: 'task_started', turn_id: 'explicit-unlabeled-final', started_at: '2026-09-30T00:00:00.000Z' } },
|
|
41
|
+
{ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'an earlier assistant message had a label' }] } },
|
|
42
|
+
{ type: 'response_item', payload: { type: 'message', role: 'assistant', phase: 'commentary', content: [{ type: 'output_text', text: 'Earlier answer.\n' + TIER1_LABEL }] } },
|
|
43
|
+
{ type: 'event_msg', timestamp: '2026-09-30T00:01:00.000Z', payload: { type: 'task_complete', turn_id: 'explicit-unlabeled-final', last_agent_message: 'This is the explicit final reply without a label.' } },
|
|
44
|
+
{ type: 'event_msg', timestamp: '2026-09-30T00:00:00.000Z', payload: { type: 'task_started', turn_id: 'unobserved-write', started_at: '2026-09-30T00:00:00.000Z' } },
|
|
45
|
+
{ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'write a small parser change' }] } },
|
|
46
|
+
{ type: 'response_item', payload: { type: 'function_call', name: 'apply_patch', arguments: '*** Begin Patch\n*** Add File: parser.js\n+module.exports = true;\n*** End Patch' } },
|
|
47
|
+
{ type: 'event_msg', timestamp: '2026-09-30T00:01:00.000Z', payload: { type: 'task_complete', turn_id: 'unobserved-write', last_agent_message: TIER1_LABEL } },
|
|
48
|
+
{ type: 'event_msg', timestamp: '2026-09-30T00:00:00.000Z', payload: { type: 'task_started', turn_id: 'not-final-assistant', started_at: '2026-09-30T00:00:00.000Z' } },
|
|
49
|
+
{ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'do not use an earlier assistant message label' }] } },
|
|
50
|
+
{ type: 'response_item', payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: LABEL }] } },
|
|
51
|
+
{ type: 'response_item', payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'A later assistant message replaced this one.' }] } },
|
|
52
|
+
{ type: 'event_msg', timestamp: '2026-09-30T00:01:00.000Z', payload: { type: 'task_complete', turn_id: 'not-final-assistant', last_agent_message: 'No label in the final assistant message.' } },
|
|
53
|
+
{ type: 'event_msg', timestamp: '2026-09-28T00:00:00.000Z', payload: { type: 'task_started', turn_id: 'missing-prompt', started_at: '2026-09-28T00:00:00.000Z' } },
|
|
54
|
+
{ type: 'event_msg', timestamp: '2026-09-28T00:01:00.000Z', payload: { type: 'task_complete', turn_id: 'missing-prompt', last_agent_message: LABEL } },
|
|
55
|
+
{ type: 'event_msg', timestamp: '2026-09-29T00:00:00.000Z', payload: { type: 'task_started', turn_id: 'invalid-label', started_at: '2026-09-29T00:00:00.000Z' } },
|
|
56
|
+
{ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'malformed label prompt' }] } },
|
|
57
|
+
{ type: 'event_msg', timestamp: '2026-09-29T00:01:00.000Z', payload: { type: 'task_complete', turn_id: 'invalid-label', last_agent_message: '<!-- harness-label {bad json} -->' } },
|
|
58
|
+
{ type: 'event_msg', timestamp: '2026-09-30T00:00:00.000Z', payload: { type: 'task_started', turn_id: 'ambiguous-summary', started_at: '2026-09-30T00:00:00.000Z' } },
|
|
59
|
+
{ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'resolve conflicting final representations' }] } },
|
|
60
|
+
{ type: 'response_item', payload: { type: 'message', role: 'assistant', phase: 'final_answer', content: [{ type: 'output_text', text: 'Actual final answer.\n' + LABEL }] } },
|
|
61
|
+
{ type: 'event_msg', timestamp: '2026-09-30T00:01:00.000Z', payload: { type: 'task_complete', turn_id: 'ambiguous-summary', last_agent_message: 'The completion summary omitted the final response text.' } },
|
|
62
|
+
{ type: 'event_msg', timestamp: '2026-09-30T00:02:00.000Z', payload: { type: 'task_started', turn_id: 'after-ambiguous', started_at: '2026-09-30T00:02:00.000Z' } },
|
|
63
|
+
{ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'continue after ambiguous final representations' }] } },
|
|
64
|
+
{ type: 'event_msg', timestamp: '2026-09-30T00:03:00.000Z', payload: { type: 'task_complete', turn_id: 'after-ambiguous', last_agent_message: 'Follow-up answer.\n' + LABEL } },
|
|
65
|
+
{ type: 'event_msg', timestamp: '2026-09-30T00:04:00.000Z', payload: { type: 'task_started', turn_id: 'equivalent-body', started_at: '2026-09-30T00:04:00.000Z' } },
|
|
66
|
+
{ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'recover an equivalent assistant body' }] } },
|
|
67
|
+
{ type: 'response_item', payload: { type: 'message', role: 'assistant', phase: 'final_answer', content: [{ type: 'output_text', text: 'Equivalent final answer.\n' + LABEL }] } },
|
|
68
|
+
{ type: 'event_msg', timestamp: '2026-09-30T00:05:00.000Z', payload: { type: 'task_complete', turn_id: 'equivalent-body', last_agent_message: 'Equivalent final answer.' } },
|
|
69
|
+
{ type: 'event_msg', timestamp: '2026-09-30T00:06:00.000Z', payload: { type: 'task_started', turn_id: 'nonstring-completion', started_at: '2026-09-30T00:06:00.000Z' } },
|
|
70
|
+
{ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'fallback from non-string completion' }] } },
|
|
71
|
+
{ type: 'response_item', payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'Non-string fallback answer.\n' + LABEL }] } },
|
|
72
|
+
{ type: 'event_msg', timestamp: '2026-09-30T00:07:00.000Z', payload: { type: 'task_complete', turn_id: 'nonstring-completion', last_agent_message: null } },
|
|
73
|
+
{ type: 'event_msg', timestamp: '2026-09-30T00:08:00.000Z', payload: { type: 'task_started', turn_id: 'invalid-with-assistant-label', started_at: '2026-09-30T00:08:00.000Z' } },
|
|
74
|
+
{ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'reject malformed completion despite assistant label' }] } },
|
|
75
|
+
{ type: 'response_item', payload: { type: 'message', role: 'assistant', phase: 'final_answer', content: [{ type: 'output_text', text: 'A different assistant body.\n' + LABEL }] } },
|
|
76
|
+
{ type: 'event_msg', timestamp: '2026-09-30T00:09:00.000Z', payload: { type: 'task_complete', turn_id: 'invalid-with-assistant-label', last_agent_message: '<!-- harness-label {bad json} -->' } },
|
|
77
|
+
];
|
|
78
|
+
fs.writeFileSync(rollout, rows.map(row => JSON.stringify(row)).join('\n') + '\n');
|
|
79
|
+
|
|
80
|
+
function runImport(targetStore = store) {
|
|
81
|
+
return spawnSync(process.execPath, [IMPORTER, '--sessions', sessions, '--store', targetStore, '--since', '2026-09-27'], {
|
|
82
|
+
cwd: ROOT,
|
|
83
|
+
encoding: 'utf8',
|
|
84
|
+
timeout: 10000,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const hookEnv = { HARNESS_OBSERVATIONS: 'on', HARNESS_OBSERVATIONS_DIR: store };
|
|
90
|
+
obs.handle({ hook_event_name: 'UserPromptSubmit', session_id: 'fixture-session', turn_id: 'included',
|
|
91
|
+
prompt: 'Please fix the parser. Contact user@example.com.', cwd: root }, hookEnv);
|
|
92
|
+
obs.handle({ hook_event_name: 'PostToolUse', session_id: 'fixture-session', turn_id: 'included', cwd: root,
|
|
93
|
+
tool_name: 'apply_patch', tool_input: { patch: '*** Begin Patch\n*** Add File: parser.js\n+module.exports = true;\n*** End Patch' } }, hookEnv);
|
|
94
|
+
obs.handle({ hook_event_name: 'Stop', session_id: 'fixture-session', turn_id: 'included', last_agent_message: TIER1_LABEL }, hookEnv);
|
|
95
|
+
|
|
96
|
+
const first = runImport();
|
|
97
|
+
assert.strictEqual(first.status, 0, 'session import failed:\n' + first.stdout + '\n' + first.stderr);
|
|
98
|
+
const report = JSON.parse(first.stdout);
|
|
99
|
+
assert.strictEqual(report.imported, 5);
|
|
100
|
+
assert.strictEqual(report.skipped.beforeCutoff, 1);
|
|
101
|
+
assert.strictEqual(report.skipped.missingPrompt, 1);
|
|
102
|
+
assert.strictEqual(report.skipped.missingLabel, 2, 'an explicit unlabeled completion remains a missing-label skip when the labeled assistant item was commentary');
|
|
103
|
+
assert.strictEqual(report.skipped.ambiguousFinalMessage, 1, 'conflicting final representations with a labeled final assistant item are reported as ambiguous');
|
|
104
|
+
assert.strictEqual(report.skipped.invalidLabel, 2, 'an invalid completion label remains invalid even when an assistant item has a valid label');
|
|
105
|
+
assert.strictEqual(report.skipped.duplicate, 1, 'backfill skips a turn already captured by live Codex hooks');
|
|
106
|
+
assert.strictEqual(report.skipped.ambiguousLiveMatch, 0);
|
|
107
|
+
|
|
108
|
+
const index = JSON.parse(fs.readFileSync(path.join(store, 'observations-index.json'), 'utf8'));
|
|
109
|
+
assert.strictEqual(index.records.length, 6);
|
|
110
|
+
const liveRecord = index.records.find(entry => entry.writer && entry.writer.turnId === 'included');
|
|
111
|
+
assert(liveRecord, 'live records retain Codex turn IDs for cross-source dedupe');
|
|
112
|
+
assert.strictEqual(liveRecord.behavior.filesWritten, 1, 'the live fixture contains an observed file write');
|
|
113
|
+
assert.deepStrictEqual(obs.contradictions(liveRecord.selfReport, liveRecord.behavior), ['tier1-with-writes']);
|
|
114
|
+
assert.strictEqual(index.records.filter(entry => entry.writer && entry.writer.sessionId === 'fixture-session' && entry.writer.turnId === 'included').length, 1,
|
|
115
|
+
'live capture and session backfill leave one record for a turn');
|
|
116
|
+
|
|
117
|
+
const fallbackRecord = index.records.find(entry => entry.captureMethod === 'session-backfill' && entry.turn === 3);
|
|
118
|
+
assert(fallbackRecord, 'the response_item assistant label is recovered when task_complete omits it');
|
|
119
|
+
const fallbackText = JSON.parse(fs.readFileSync(path.join(store, 'text', fallbackRecord.contentSha256 + '.json'), 'utf8'));
|
|
120
|
+
assert.strictEqual(fallbackText.prompt, 'fallback prompt from the rollout message');
|
|
121
|
+
const backfilledTier1 = index.records.find(entry => entry.captureMethod === 'session-backfill' && entry.selfReport.tier === 'tier1');
|
|
122
|
+
assert(backfilledTier1, 'the tier1 label from the rollout is imported');
|
|
123
|
+
assert.strictEqual(backfilledTier1.behavior, null, 'session backfill records behavior as unobserved rather than zero');
|
|
124
|
+
assert(!index.records.some(entry => entry.writer && entry.writer.turnId === 'ambiguous-summary'),
|
|
125
|
+
'a differing completion string must not inherit the assistant message label');
|
|
126
|
+
const afterAmbiguous = index.records.find(entry => entry.writer && entry.writer.turnId === 'after-ambiguous');
|
|
127
|
+
assert(afterAmbiguous, 'the turn after an ambiguous final can still be imported');
|
|
128
|
+
const afterAmbiguousText = JSON.parse(fs.readFileSync(path.join(store, 'text', afterAmbiguous.contentSha256 + '.json'), 'utf8'));
|
|
129
|
+
assert.strictEqual(afterAmbiguousText.previous, '', 'an unresolved final disagreement must not seed later previous-message context');
|
|
130
|
+
assert(index.records.some(entry => entry.writer && entry.writer.turnId === 'equivalent-body'),
|
|
131
|
+
'a completion string matching the assistant body after label removal safely recovers its label');
|
|
132
|
+
assert(index.records.some(entry => entry.writer && entry.writer.turnId === 'nonstring-completion'),
|
|
133
|
+
'a non-string task_complete field falls back to the final assistant label');
|
|
134
|
+
assert(!index.records.some(entry => entry.writer && entry.writer.turnId === 'invalid-with-assistant-label'),
|
|
135
|
+
'a valid assistant label never replaces an invalid completion label');
|
|
136
|
+
|
|
137
|
+
const exportDir = path.join(root, 'export');
|
|
138
|
+
const exported = spawnSync(process.execPath, [EXPORTER, '--store', store, '--out', exportDir, '--self-report-only'], { cwd: ROOT, encoding: 'utf8', timeout: 10000 });
|
|
139
|
+
assert.strictEqual(exported.status, 0, 'export after backfill failed:\n' + exported.stdout + '\n' + exported.stderr);
|
|
140
|
+
const labels = fs.readFileSync(path.join(exportDir, 'labels-observed.jsonl'), 'utf8').trim().split('\n').map(JSON.parse);
|
|
141
|
+
const exportReport = JSON.parse(fs.readFileSync(path.join(exportDir, 'export-report.json'), 'utf8'));
|
|
142
|
+
assert.strictEqual(labels.filter(label => label.behaviorEvidence === 'unobserved').length, 5);
|
|
143
|
+
assert.deepStrictEqual(exportReport.behaviorEvidence, { observed: 1, unobserved: 5 });
|
|
144
|
+
assert.strictEqual(exportReport.contradictions['tier1-with-writes'], 1, 'only the observed live write contributes a contradiction');
|
|
145
|
+
|
|
146
|
+
const reviewHtml = path.join(root, 'review.html');
|
|
147
|
+
const reviewed = spawnSync(process.execPath, [REVIEW, '--store', store, '--out', reviewHtml], { cwd: ROOT, encoding: 'utf8', timeout: 10000 });
|
|
148
|
+
assert.strictEqual(reviewed.status, 0, 'observation review page failed:\n' + reviewed.stdout + '\n' + reviewed.stderr);
|
|
149
|
+
assert.match(fs.readFileSync(reviewHtml, 'utf8'), /Behavior evidence is unobserved/);
|
|
150
|
+
|
|
151
|
+
const legacyStore = path.join(root, 'legacy-observations');
|
|
152
|
+
fs.mkdirSync(legacyStore, { recursive: true });
|
|
153
|
+
fs.writeFileSync(path.join(legacyStore, 'observations-index.json'), JSON.stringify({ schemaVersion: 1, records: [
|
|
154
|
+
{
|
|
155
|
+
id: 'legacy-live-record', source: 'observation', host: 'codex', turn: 2,
|
|
156
|
+
writer: { sessionId: 'fixture-session' }, contentSha256: 'legacy-content-hash', behavior: { filesWritten: 1 }
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: 'old-backfill-record', source: 'observation', captureMethod: 'session-backfill', host: 'codex', turn: 1,
|
|
160
|
+
writer: { sessionId: 'older-session', turnId: 'old' }, behavior: { filesWritten: 0, toolCalls: 0 }
|
|
161
|
+
}
|
|
162
|
+
] }));
|
|
163
|
+
const legacy = runImport(legacyStore);
|
|
164
|
+
assert.strictEqual(legacy.status, 0, legacy.stderr);
|
|
165
|
+
const legacyReport = JSON.parse(legacy.stdout);
|
|
166
|
+
assert.strictEqual(legacyReport.skipped.ambiguousLiveMatch, 1,
|
|
167
|
+
'legacy live records without a turn ID are explicitly skipped when the same session/turn is ambiguous');
|
|
168
|
+
assert.strictEqual(legacyReport.migratedUnobservedBehavior, 1, 'prior backfill records are migrated away from synthetic zero behavior');
|
|
169
|
+
const legacyIndex = JSON.parse(fs.readFileSync(path.join(legacyStore, 'observations-index.json'), 'utf8'));
|
|
170
|
+
assert.strictEqual(legacyIndex.records.find(entry => entry.id === 'old-backfill-record').behavior, null);
|
|
171
|
+
|
|
172
|
+
const second = runImport();
|
|
173
|
+
assert.strictEqual(second.status, 0, second.stderr);
|
|
174
|
+
assert.strictEqual(JSON.parse(second.stdout).imported, 0, 'repeated imports must not duplicate records');
|
|
175
|
+
assert.strictEqual(JSON.parse(second.stdout).skipped.duplicate, 6);
|
|
176
|
+
assert.strictEqual(JSON.parse(fs.readFileSync(path.join(store, 'observations-index.json'), 'utf8')).records.length, 6);
|
|
177
|
+
|
|
178
|
+
const lockSessions = path.join(root, 'lock-timeout-sessions');
|
|
179
|
+
fs.mkdirSync(lockSessions, { recursive: true });
|
|
180
|
+
const lockRollout = path.join(lockSessions, 'lock-timeout.jsonl');
|
|
181
|
+
fs.writeFileSync(lockRollout, [
|
|
182
|
+
{ type: 'event_msg', timestamp: '2026-10-01T00:00:00.000Z', payload: { type: 'task_started', turn_id: 'lock-timeout', started_at: '2026-10-01T00:00:00.000Z' } },
|
|
183
|
+
{ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'a unique turn that cannot be persisted while locked' }] } },
|
|
184
|
+
{ type: 'event_msg', timestamp: '2026-10-01T00:01:00.000Z', payload: { type: 'task_complete', turn_id: 'lock-timeout', last_agent_message: LABEL } },
|
|
185
|
+
].map(row => JSON.stringify(row)).join('\n') + '\n');
|
|
186
|
+
const lockStore = path.join(root, 'lock-timeout-store');
|
|
187
|
+
fs.mkdirSync(lockStore, { recursive: true });
|
|
188
|
+
fs.writeFileSync(path.join(lockStore, '.index.lock'), 'held for importer timeout fixture');
|
|
189
|
+
const locked = spawnSync(process.execPath, [IMPORTER, '--sessions', lockSessions, '--store', lockStore, '--since', '2026-09-27'], {
|
|
190
|
+
cwd: ROOT,
|
|
191
|
+
encoding: 'utf8',
|
|
192
|
+
timeout: 10000,
|
|
193
|
+
});
|
|
194
|
+
assert.strictEqual(locked.status, 1, 'an importer lock timeout must return a failing CLI exit code');
|
|
195
|
+
const lockReport = JSON.parse(locked.stdout);
|
|
196
|
+
assert.strictEqual(lockReport.failed.lockTimeout, 1, 'the report distinguishes a lock timeout from a duplicate');
|
|
197
|
+
assert.strictEqual(fs.existsSync(path.join(lockStore, 'observations-index.json')), false,
|
|
198
|
+
'a unique turn must not be reported as persisted when the index lock times out');
|
|
199
|
+
|
|
200
|
+
const migrationStore = path.join(root, 'migration-timeout-store');
|
|
201
|
+
fs.mkdirSync(migrationStore, { recursive: true });
|
|
202
|
+
fs.writeFileSync(path.join(migrationStore, 'observations-index.json'), JSON.stringify({ schemaVersion: 1, records: [
|
|
203
|
+
{ id: 'old-backfill', captureMethod: 'session-backfill', behavior: { filesWritten: 0 } }
|
|
204
|
+
] }));
|
|
205
|
+
fs.writeFileSync(path.join(migrationStore, '.index.lock'), 'held during migration fixture');
|
|
206
|
+
const migrationLocked = spawnSync(process.execPath, [IMPORTER, '--sessions', lockSessions, '--store', migrationStore, '--since', '2026-09-27'], {
|
|
207
|
+
cwd: ROOT,
|
|
208
|
+
encoding: 'utf8',
|
|
209
|
+
timeout: 10000,
|
|
210
|
+
});
|
|
211
|
+
assert.strictEqual(migrationLocked.status, 1, 'a migration lock timeout must also fail the importer CLI');
|
|
212
|
+
assert.strictEqual(JSON.parse(migrationLocked.stdout).failed.lockTimeout, 1,
|
|
213
|
+
'the migration path reports its lock timeout through the same failure count');
|
|
214
|
+
const migrationIndex = JSON.parse(fs.readFileSync(path.join(migrationStore, 'observations-index.json'), 'utf8'));
|
|
215
|
+
assert.deepStrictEqual(migrationIndex.records[0].behavior, { filesWritten: 0 },
|
|
216
|
+
'a failed migration does not rewrite the index or proceed with imports');
|
|
217
|
+
} finally {
|
|
218
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
console.log('PASS: post-revision Codex session labels import as redacted, prompt-paired, idempotent observations.');
|