throughline 0.6.0 → 0.6.2

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +46 -5
  3. package/bin/throughline.mjs +41 -0
  4. package/docs/00_overview.md +2 -0
  5. package/docs/04_public_release_plan.md +2 -0
  6. package/docs/12_desktop_clear_handoff_plan.md +3 -3
  7. package/docs/13_native_factory_diagnostics_plan.md +46 -0
  8. package/docs/BUGHUB_RUNTIME_ERROR_STORE_PLAN.md +52 -0
  9. package/package.json +2 -2
  10. package/src/auditor-context.mjs +330 -0
  11. package/src/auditor-context.test.mjs +303 -0
  12. package/src/cli/auditor-context.mjs +141 -0
  13. package/src/cli/auditor-context.test.mjs +148 -0
  14. package/src/cli/codex-hook.mjs +27 -4
  15. package/src/cli/codex-hook.test.mjs +4 -0
  16. package/src/cli/codex-restore-smoke.mjs +2 -1
  17. package/src/cli/codex-restore-source-audit.mjs +1 -1
  18. package/src/cli/doctor.mjs +5 -1
  19. package/src/cli/factory-diagnostics.mjs +246 -0
  20. package/src/cli/factory-diagnostics.test.mjs +201 -0
  21. package/src/cli/runtime-errors.mjs +85 -0
  22. package/src/cli/runtime-errors.test.mjs +75 -0
  23. package/src/cli/trim.mjs +4 -4
  24. package/src/codex-handoff-model-smoke.mjs +2 -3
  25. package/src/codex-sidecar-cli.test.mjs +19 -8
  26. package/src/codex-sidecar.mjs +2 -5
  27. package/src/codex-sidecar.test.mjs +17 -9
  28. package/src/codex-thread-index.mjs +17 -2
  29. package/src/db.mjs +1 -1
  30. package/src/factory-diagnostics.mjs +118 -0
  31. package/src/factory-diagnostics.test.mjs +97 -0
  32. package/src/haiku-summarizer.mjs +3 -5
  33. package/src/haiku-summarizer.test.mjs +52 -47
  34. package/src/hook-entrypoints.test.mjs +3 -1
  35. package/src/phase0-spotter-contract.test.mjs +279 -0
  36. package/src/portable-spawn-sync.mjs +58 -0
  37. package/src/portable-spawn-sync.test.mjs +40 -0
  38. package/src/prompt-submit.mjs +2 -0
  39. package/src/runtime-error-hook.test.mjs +106 -0
  40. package/src/runtime-error-observer.mjs +8 -0
  41. package/src/runtime-error-store.mjs +595 -0
  42. package/src/runtime-error-store.test.mjs +307 -0
  43. package/src/session-start.mjs +2 -0
  44. package/src/test-env.mjs +59 -2
  45. package/src/turn-backfill.test.mjs +2 -2
  46. package/src/turn-processor.mjs +2 -0
  47. package/src/windows-acl-test-helper.mjs +29 -0
@@ -0,0 +1,307 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { spawn } from 'node:child_process';
4
+ import { chmodSync, mkdtempSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { dirname, join } from 'node:path';
7
+
8
+ import {
9
+ acknowledgeRuntimeErrors,
10
+ compactRuntimeErrors,
11
+ defaultFactoryReporterConfigPath,
12
+ defaultRuntimeErrorStorePath,
13
+ getRuntimeErrorDiagnostics,
14
+ observeRuntimeError,
15
+ readRuntimeErrorSnapshot,
16
+ reopenRuntimeError,
17
+ resolveRuntimeError,
18
+ } from './runtime-error-store.mjs';
19
+ import { applyWindowsPrivateAcl } from './windows-acl-test-helper.mjs';
20
+
21
+ const TEST_PLATFORM = process.platform === 'win32' ? 'win32' : 'darwin';
22
+
23
+ function sandbox() {
24
+ const root = mkdtempSync(join(tmpdir(), 'throughline-runtime-errors-'));
25
+ const env = {
26
+ HOME: root,
27
+ USERPROFILE: root,
28
+ LOCALAPPDATA: root,
29
+ XDG_CONFIG_HOME: join(root, 'config'),
30
+ XDG_STATE_HOME: join(root, 'state'),
31
+ };
32
+ const configPath = defaultFactoryReporterConfigPath(env);
33
+ const storePath = defaultRuntimeErrorStorePath(env);
34
+ return { root, env, configPath, storePath };
35
+ }
36
+
37
+ function enableCollection(box, reporting = { enabled: false }) {
38
+ mkdirSync(dirname(box.configPath), { recursive: true });
39
+ writeFileSync(box.configPath, JSON.stringify({
40
+ schema_version: '1.0',
41
+ host: { id: 'test-host', profile: process.platform === 'win32' ? 'windows-native' : 'mac' },
42
+ collection: { enabled: true },
43
+ reporting,
44
+ }));
45
+ applyWindowsPrivateAcl(box.configPath);
46
+ }
47
+
48
+ test('runtime error store: missing/false/malformed config is fail-closed and creates no state', () => {
49
+ for (const config of [
50
+ null,
51
+ { collection: { enabled: true } },
52
+ { collection: { enabled: false } },
53
+ { collection: { enabled: 'true' } },
54
+ '{malformed',
55
+ ]) {
56
+ const box = sandbox();
57
+ if (config !== null) {
58
+ mkdirSync(dirname(box.configPath), { recursive: true });
59
+ writeFileSync(box.configPath, typeof config === 'string' ? config : JSON.stringify(config));
60
+ applyWindowsPrivateAcl(box.configPath);
61
+ }
62
+ assert.deepEqual(observeRuntimeError({ code: 'HOOK_PROCESS_TURN_FAILED' }, { env: box.env }), {
63
+ status: 'disabled',
64
+ });
65
+ assert.equal(getRuntimeErrorDiagnostics({ env: box.env }).collection, 'disabled');
66
+ assert.throws(() => statSync(box.storePath), { code: 'ENOENT' });
67
+ }
68
+ });
69
+
70
+ test('runtime error store: Windows native uses the canonical LocalAppData paths', () => {
71
+ const env = {
72
+ OS: 'Windows_NT',
73
+ USERPROFILE: 'C:\\Users\\kite_',
74
+ LOCALAPPDATA: 'C:\\Users\\kite_\\AppData\\Local',
75
+ };
76
+ assert.equal(
77
+ defaultFactoryReporterConfigPath(env),
78
+ join(env.LOCALAPPDATA, 'dotagents', 'factory-reporter', 'config.json'),
79
+ );
80
+ assert.equal(
81
+ defaultRuntimeErrorStorePath(env),
82
+ join(env.LOCALAPPDATA, 'throughline', 'runtime-errors.json'),
83
+ );
84
+ });
85
+
86
+ test('runtime error store: reporting config and credentials are ignored and no network API is accepted', () => {
87
+ const box = sandbox();
88
+ enableCollection(box, {
89
+ enabled: true,
90
+ endpoint: 'https://should-never-be-read.invalid/private',
91
+ credential_file: process.platform === 'win32' ? 'C:\\private\\token' : '/private/token',
92
+ });
93
+ const result = observeRuntimeError(
94
+ { code: 'HOOK_PROCESS_TURN_FAILED', now: '2026-07-13T00:00:00.000Z' },
95
+ { env: box.env, version: '0.6.1', platform: TEST_PLATFORM, arch: 'arm64' },
96
+ );
97
+ assert.equal(result.status, 'recorded');
98
+ const bytes = readFileSync(box.storePath, 'utf8');
99
+ assert.doesNotMatch(bytes, /should-never-be-read|private\/token|endpoint|credential/i);
100
+ });
101
+
102
+ test('runtime error store: observation API rejects raw or arbitrary fields', () => {
103
+ const box = sandbox();
104
+ enableCollection(box);
105
+ for (const forbidden of [
106
+ { exception: new Error('secret') },
107
+ { stderr: 'raw stderr' },
108
+ { stack: 'raw stack' },
109
+ { prompt: 'private prompt' },
110
+ { session: 'session-id' },
111
+ { path: '/Users/private/file' },
112
+ { context: { arbitrary: true } },
113
+ ]) {
114
+ assert.throws(
115
+ () => observeRuntimeError({ code: 'HOOK_PROCESS_TURN_FAILED', ...forbidden }, { env: box.env }),
116
+ /固定 code と時刻だけ/,
117
+ );
118
+ }
119
+ assert.throws(
120
+ () => observeRuntimeError(
121
+ { code: 'HOOK_PROCESS_TURN_FAILED' },
122
+ { env: box.env, stderr: 'raw stderr' },
123
+ ),
124
+ /未定義 option/,
125
+ );
126
+ assert.throws(
127
+ () => observeRuntimeError({ code: 'UNKNOWN_FAILURE' }, { env: box.env }),
128
+ /未登録の runtime error code/,
129
+ );
130
+ assert.throws(() => statSync(box.storePath), { code: 'ENOENT' });
131
+ });
132
+
133
+ test('runtime error store: fixed template SHA-256 fingerprint aggregates and reopens', () => {
134
+ const box = sandbox();
135
+ enableCollection(box);
136
+ const options = { env: box.env, version: '0.6.1', platform: TEST_PLATFORM, arch: 'arm64' };
137
+ const first = observeRuntimeError(
138
+ { code: 'HOOK_PROCESS_TURN_FAILED', now: '2026-07-13T00:00:00.000Z' }, options,
139
+ );
140
+ const second = observeRuntimeError(
141
+ { code: 'HOOK_PROCESS_TURN_FAILED', now: '2026-07-13T00:01:00.000Z' }, options,
142
+ );
143
+ assert.match(first.fingerprint, /^[0-9a-f]{64}$/);
144
+ assert.equal(second.fingerprint, first.fingerprint);
145
+
146
+ let snapshot = readRuntimeErrorSnapshot({ env: box.env });
147
+ assert.equal(snapshot.runtime_errors.length, 1);
148
+ assert.deepEqual(snapshot.runtime_errors[0], {
149
+ error_code: 'HOOK_PROCESS_TURN_FAILED',
150
+ component: 'claude_stop_hook',
151
+ status: 'open',
152
+ severity: 'high',
153
+ fingerprint: first.fingerprint,
154
+ message_template: 'Throughline Claude Stop hook processing failed',
155
+ occurrence_count: 2,
156
+ first_seen: '2026-07-13T00:00:00.000Z',
157
+ last_seen: '2026-07-13T00:01:00.000Z',
158
+ state_schema_version: '1.0',
159
+ });
160
+
161
+ const resolved = resolveRuntimeError(first.fingerprint, {
162
+ env: box.env,
163
+ now: '2026-07-13T00:02:00.000Z',
164
+ });
165
+ assert.equal(resolved.status, 'resolved');
166
+ snapshot = readRuntimeErrorSnapshot({ env: box.env });
167
+ assert.equal(snapshot.runtime_errors.length, 0);
168
+ assert.deepEqual(snapshot.resolutions, [{
169
+ fingerprint: first.fingerprint,
170
+ resolved_at: '2026-07-13T00:02:00.000Z',
171
+ reason_code: 'manual',
172
+ }]);
173
+ assert.equal(reopenRuntimeError(first.fingerprint, { env: box.env }).status, 'open');
174
+ assert.equal(readRuntimeErrorSnapshot({ env: box.env }).resolutions.length, 0);
175
+ resolveRuntimeError(first.fingerprint, { env: box.env, now: '2026-07-13T00:02:30.000Z', reasonCode: 'recovered' });
176
+
177
+ observeRuntimeError(
178
+ { code: 'HOOK_PROCESS_TURN_FAILED', now: '2026-07-13T00:03:00.000Z' }, options,
179
+ );
180
+ snapshot = readRuntimeErrorSnapshot({ env: box.env });
181
+ assert.equal(snapshot.runtime_errors[0].status, 'open');
182
+ assert.equal(snapshot.runtime_errors[0].occurrence_count, 3);
183
+ assert.equal(snapshot.runtime_errors[0].last_seen, '2026-07-13T00:03:00.000Z');
184
+ });
185
+
186
+ test('runtime error store: cursor/ack are monotonic and snapshots are bounded', () => {
187
+ const box = sandbox();
188
+ enableCollection(box);
189
+ const options = { env: box.env, version: '0.6.1' };
190
+ observeRuntimeError({ code: 'HOOK_PROCESS_TURN_FAILED', now: '2026-07-13T00:00:00.000Z' }, options);
191
+ observeRuntimeError({ code: 'HOOK_SESSION_START_FAILED', now: '2026-07-13T00:01:00.000Z' }, options);
192
+ observeRuntimeError({ code: 'HOOK_PROMPT_SUBMIT_FAILED', now: '2026-07-13T00:02:00.000Z' }, options);
193
+
194
+ const page = readRuntimeErrorSnapshot({ env: box.env, afterCursor: 0, limit: 2 });
195
+ assert.equal(page.runtime_errors.length, 2);
196
+ assert.equal(page.diagnostics.truncated, true);
197
+ assert.equal(page.cursor.high_watermark, 3);
198
+ assert.equal(page.cursor.acknowledged_through, 0);
199
+ assert.equal(acknowledgeRuntimeErrors(2, { env: box.env }).acknowledgedThrough, 2);
200
+ assert.equal(acknowledgeRuntimeErrors(1, { env: box.env }).acknowledgedThrough, 2);
201
+ assert.throws(() => acknowledgeRuntimeErrors(999, { env: box.env }), /high watermark/);
202
+ assert.equal(readRuntimeErrorSnapshot({ env: box.env }).cursor.acknowledged_through, 2);
203
+ });
204
+
205
+ test('runtime error store: compact removes only acknowledged resolved expired records', () => {
206
+ const box = sandbox();
207
+ enableCollection(box);
208
+ const options = { env: box.env, version: '0.6.1' };
209
+ const old = observeRuntimeError(
210
+ { code: 'HOOK_PROCESS_TURN_FAILED', now: '2026-06-01T00:00:00.000Z' }, options,
211
+ );
212
+ const pending = observeRuntimeError(
213
+ { code: 'HOOK_SESSION_START_FAILED', now: '2026-06-01T00:00:00.000Z' }, options,
214
+ );
215
+ resolveRuntimeError(old.fingerprint, { env: box.env, now: '2026-06-02T00:00:00.000Z' });
216
+ resolveRuntimeError(pending.fingerprint, { env: box.env, now: '2026-06-02T00:00:00.000Z' });
217
+ acknowledgeRuntimeErrors(3, { env: box.env });
218
+
219
+ const result = compactRuntimeErrors({
220
+ env: box.env,
221
+ now: '2026-07-13T00:00:00.000Z',
222
+ retentionMs: 30 * 24 * 60 * 60 * 1000,
223
+ });
224
+ assert.equal(result.removed, 1);
225
+ const snapshot = readRuntimeErrorSnapshot({ env: box.env });
226
+ assert.equal(snapshot.runtime_errors.length, 0);
227
+ assert.equal(snapshot.resolutions.length, 1);
228
+ assert.equal(snapshot.resolutions[0].fingerprint, pending.fingerprint);
229
+ });
230
+
231
+ test('runtime error store: atomic private store has owner-only modes and bounded diagnostics', () => {
232
+ const box = sandbox();
233
+ enableCollection(box);
234
+ observeRuntimeError({ code: 'HOOK_CODEX_FAILED' }, { env: box.env, version: '0.6.1' });
235
+ if (process.platform !== 'win32') {
236
+ assert.equal(statSync(dirname(box.storePath)).mode & 0o777, 0o700);
237
+ assert.equal(statSync(box.storePath).mode & 0o777, 0o600);
238
+ }
239
+ assert.doesNotThrow(() => JSON.parse(readFileSync(box.storePath, 'utf8')));
240
+
241
+ const diagnostics = getRuntimeErrorDiagnostics({ env: box.env });
242
+ assert.deepEqual(Object.keys(diagnostics).sort(), [
243
+ 'acknowledged_through', 'collection', 'high_watermark', 'open_count',
244
+ 'pending_count', 'schema', 'status', 'total_count',
245
+ ]);
246
+ assert.doesNotMatch(JSON.stringify(diagnostics), /Users|\\|\.throughline|runtime-errors\.json/);
247
+ });
248
+
249
+ test('runtime error store: non-canonical reporting values keep collection fail-closed', () => {
250
+ const box = sandbox();
251
+ enableCollection(box, { enabled: true, endpoint: 'ftp://invalid', credential_file: '' });
252
+ assert.deepEqual(observeRuntimeError({ code: 'HOOK_CODEX_FAILED' }, { env: box.env }), { status: 'disabled' });
253
+ });
254
+
255
+ test('runtime error store: unknown top-level fields and future ack are rejected before compaction', () => {
256
+ const box = sandbox();
257
+ enableCollection(box);
258
+ const captured = observeRuntimeError({ code: 'HOOK_CODEX_FAILED', now: '2026-06-01T00:00:00.000Z' }, { env: box.env });
259
+ resolveRuntimeError(captured.fingerprint, { env: box.env, now: '2026-06-02T00:00:00.000Z' });
260
+ const store = JSON.parse(readFileSync(box.storePath, 'utf8'));
261
+ store.secret = '/Users/private Bearer secret-token';
262
+ store.acknowledged_through = 999;
263
+ writeFileSync(box.storePath, JSON.stringify(store), { mode: 0o600 });
264
+ assert.equal(getRuntimeErrorDiagnostics({ env: box.env }).status, 'unavailable');
265
+ assert.throws(() => compactRuntimeErrors({ env: box.env, now: '2026-07-13T00:00:00.000Z', retentionMs: 0 }));
266
+ });
267
+
268
+ test('runtime error store: mode drift is unavailable and a crashed SQLite lock owner is released by the OS', { skip: process.platform === 'win32' }, async () => {
269
+ const box = sandbox();
270
+ enableCollection(box);
271
+ observeRuntimeError({ code: 'HOOK_CODEX_FAILED' }, { env: box.env });
272
+ chmodSync(box.storePath, 0o644);
273
+ assert.equal(getRuntimeErrorDiagnostics({ env: box.env }).status, 'unavailable');
274
+ chmodSync(box.storePath, 0o600);
275
+ const lockPath = `${box.storePath}.lock.sqlite`;
276
+ const script = `import {DatabaseSync} from 'node:sqlite'; const db=new DatabaseSync(${JSON.stringify(lockPath)}); db.exec('BEGIN IMMEDIATE'); console.log('READY'); setInterval(()=>{},1000);`;
277
+ const child = spawn(process.execPath, ['--input-type=module', '-e', script], { stdio: ['ignore', 'pipe', 'ignore'] });
278
+ await new Promise((resolve, reject) => {
279
+ child.stdout.setEncoding('utf8');
280
+ child.stdout.once('data', (chunk) => String(chunk).includes('READY') ? resolve() : reject(new Error('lock fixture did not start')));
281
+ child.once('error', reject);
282
+ });
283
+ child.kill('SIGKILL');
284
+ await new Promise((resolve) => child.once('exit', resolve));
285
+ assert.equal(observeRuntimeError({ code: 'HOOK_CODEX_FAILED' }, { env: box.env }).status, 'recorded');
286
+ });
287
+
288
+ test('runtime error store: atomic lock publication preserves all concurrent process observations', {
289
+ skip: process.platform === 'win32' ? 'SQLite排他はPOSIX matrix、Windowsはnative ACL/store試験で固定' : undefined,
290
+ }, async () => {
291
+ const box = sandbox();
292
+ enableCollection(box);
293
+ const modulePath = new URL('./runtime-error-store.mjs', import.meta.url).href;
294
+ const script = `import {observeRuntimeError} from ${JSON.stringify(modulePath)}; observeRuntimeError({code:'HOOK_CODEX_FAILED'});`;
295
+ const results = await Promise.all(Array.from({ length: 20 }, () => new Promise((resolve) => {
296
+ const child = spawn(process.execPath, ['--input-type=module', '-e', script], {
297
+ env: { ...process.env, ...box.env }, stdio: ['ignore', 'ignore', 'pipe'],
298
+ });
299
+ let stderr = '';
300
+ child.stderr.setEncoding('utf8');
301
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
302
+ child.once('exit', (code) => resolve({ code, stderr }));
303
+ })));
304
+ assert.deepEqual(results, Array.from({ length: 20 }, () => ({ code: 0, stderr: '' })));
305
+ const snapshot = readRuntimeErrorSnapshot({ env: box.env });
306
+ assert.equal(snapshot.runtime_errors[0].occurrence_count, 20);
307
+ });
@@ -37,6 +37,7 @@ import { randomBytes } from 'node:crypto';
37
37
  import { join, dirname } from 'node:path';
38
38
  import { homedir } from 'node:os';
39
39
  import { pathToFileURL } from 'node:url';
40
+ import { recordRuntimeErrorBestEffort } from './runtime-error-store.mjs';
40
41
 
41
42
  // SPIKE ONLY — Phase 0-2 / 0-4 検証用。marker file 削除で無効化される。
42
43
  // docs/10_transcript_injection_plan.md §3 Phase 0-2 参照。
@@ -318,6 +319,7 @@ export async function run() {
318
319
 
319
320
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
320
321
  run().catch((err) => {
322
+ recordRuntimeErrorBestEffort('HOOK_SESSION_START_FAILED');
321
323
  process.stderr.write(`[session-start] error: ${err.message}\n`);
322
324
  process.exit(1);
323
325
  });
package/src/test-env.mjs CHANGED
@@ -1,4 +1,61 @@
1
1
  // Keep tests hermetic when they are run from inside a live Codex session.
2
2
  // Individual tests that need these values set them explicitly in child env.
3
- delete process.env.THROUGHLINE_CODEX_THREAD_ID;
4
- delete process.env.CODEX_THREAD_ID;
3
+ if (process.env.THROUGHLINE_TEST_ENV_ACTIVE !== '1') {
4
+ delete process.env.THROUGHLINE_CODEX_THREAD_ID;
5
+ delete process.env.CODEX_THREAD_ID;
6
+ process.env.THROUGHLINE_TEST_ENV_ACTIVE = '1';
7
+ }
8
+
9
+ // node:sqlite emits an ExperimentalWarning on Node 22. Several CLI tests
10
+ // intentionally assert their child process stderr contract, so suppress the
11
+ // runtime warning for test children without changing production stderr.
12
+ process.env.NODE_NO_WARNINGS = '1';
13
+
14
+ // Windows cannot execute the POSIX shebang used by the ephemeral fake Codex
15
+ // app-server fixtures. Run their .mjs bodies with this test runner's Node
16
+ // executable, preserving the command arguments the production code sends.
17
+ import childProcess from 'node:child_process';
18
+ import { readFileSync } from 'node:fs';
19
+ import { syncBuiltinESMExports } from 'node:module';
20
+
21
+ const { spawn: nativeSpawn, spawnSync: nativeSpawnSync } = childProcess;
22
+
23
+ function normalizeNodeFixture(command, args = []) {
24
+ if (process.platform !== 'win32' || typeof command !== 'string' || !Array.isArray(args)) {
25
+ return [command, args];
26
+ }
27
+ if (command.endsWith('.mjs')) return [process.execPath, [command, ...args]];
28
+ try {
29
+ const header = readFileSync(command, 'utf8').slice(0, 128);
30
+ if (/^#!.*\bnode(?:\.exe)?\b/.test(header)) return [process.execPath, [command, ...args]];
31
+ if (/^#!.*\b(?:ba)?sh\b/.test(header)) return ['bash', [command, ...args]];
32
+ } catch {
33
+ // Missing/non-file commands must retain their native spawn error contract.
34
+ }
35
+ return [command, args];
36
+ }
37
+
38
+ childProcess.spawn = function spawn(command, args, options) {
39
+ const [normalizedCommand, normalizedArgs] = normalizeNodeFixture(command, args);
40
+ return nativeSpawn(normalizedCommand, normalizedArgs, options);
41
+ };
42
+
43
+ childProcess.spawnSync = function spawnSync(command, args, options) {
44
+ const [normalizedCommand, normalizedArgs] = normalizeNodeFixture(command, args);
45
+ return nativeSpawnSync(normalizedCommand, normalizedArgs, {
46
+ maxBuffer: 16 * 1024 * 1024,
47
+ ...options,
48
+ });
49
+ };
50
+
51
+ syncBuiltinESMExports();
52
+
53
+ // CLI tests start nested Node processes which in turn launch the fixture
54
+ // executable. Carry this test-only adapter into those children on Windows.
55
+ if (process.platform === 'win32') {
56
+ const importOption = `--import=${import.meta.url}`;
57
+ const current = process.env.NODE_OPTIONS?.trim() ?? '';
58
+ if (!current.includes(importOption)) {
59
+ process.env.NODE_OPTIONS = current ? `${current} ${importOption}` : importOption;
60
+ }
61
+ }
@@ -207,7 +207,7 @@ test('backfillBodies: sidechain entries and missing or empty paths produce no gr
207
207
 
208
208
  test('deriveTranscriptPath munges slash and dot characters with one leading dash', () => {
209
209
  assert.equal(
210
- deriveTranscriptPath('/Users/kite/Developer/Through.line', 'session-id'),
211
- join(homedir(), '.claude', 'projects', '-Users-kite-Developer-Through-line', 'session-id.jsonl'),
210
+ deriveTranscriptPath('/Users/example/Developer/Through.line', 'session-id'),
211
+ join(homedir(), '.claude', 'projects', '-Users-example-Developer-Through-line', 'session-id.jsonl'),
212
212
  );
213
213
  });
@@ -43,6 +43,7 @@ import { summarizeToL1 } from './haiku-summarizer.mjs';
43
43
  import { ensureMonitorTaskFile } from './vscode-task.mjs';
44
44
  import { readLatestUsage } from './transcript-usage.mjs';
45
45
  import { pathToFileURL } from 'node:url';
46
+ import { recordRuntimeErrorBestEffort } from './runtime-error-store.mjs';
46
47
 
47
48
  /** 直近 N ターンは bodies を生で残し、それより古いものだけ L1 要約する。 */
48
49
  export const L2_WINDOW = 20;
@@ -291,6 +292,7 @@ export async function run() {
291
292
 
292
293
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
293
294
  run().catch((err) => {
295
+ recordRuntimeErrorBestEffort('HOOK_PROCESS_TURN_FAILED');
294
296
  const msg = err instanceof Error ? err.message : String(err);
295
297
  process.stderr.write(`[turn-processor] error: ${msg}\n`);
296
298
  process.exit(1);
@@ -0,0 +1,29 @@
1
+ import { spawnSync } from 'node:child_process';
2
+
3
+ export function applyWindowsPrivateAcl(path, directory = false) {
4
+ if (process.platform !== 'win32') return;
5
+ const script = String.raw`
6
+ $ErrorActionPreference='Stop'
7
+ $target=$env:THROUGHLINE_TEST_ACL_PATH; $isDir=$env:THROUGHLINE_TEST_ACL_DIRECTORY -eq '1'
8
+ $sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User
9
+ $acl=if($isDir){New-Object System.Security.AccessControl.DirectorySecurity}else{New-Object System.Security.AccessControl.FileSecurity}
10
+ $acl.SetOwner($sid); $acl.SetAccessRuleProtection($true,$false)
11
+ $inherit=if($isDir){[System.Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit'}else{[System.Security.AccessControl.InheritanceFlags]::None}
12
+ $rule=New-Object System.Security.AccessControl.FileSystemAccessRule($sid,'FullControl',$inherit,[System.Security.AccessControl.PropagationFlags]::None,[System.Security.AccessControl.AccessControlType]::Allow)
13
+ $acl.AddAccessRule($rule)
14
+ if($isDir){[System.IO.Directory]::SetAccessControl($target,$acl)}else{[System.IO.File]::SetAccessControl($target,$acl)}
15
+ `;
16
+ const result = spawnSync('powershell.exe', [
17
+ '-NoProfile', '-NonInteractive', '-Command', script,
18
+ ], {
19
+ encoding: 'utf8',
20
+ timeout: 3_000,
21
+ windowsHide: true,
22
+ env: {
23
+ ...process.env,
24
+ THROUGHLINE_TEST_ACL_PATH: path,
25
+ THROUGHLINE_TEST_ACL_DIRECTORY: directory ? '1' : '0',
26
+ },
27
+ });
28
+ if (result.status !== 0) throw new Error(result.stderr || 'Windows ACL fixture setup failed');
29
+ }