throughline 0.3.23 → 0.3.25

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 (111) hide show
  1. package/.claude/commands/tl-trim.md +42 -0
  2. package/.codex-sidecar.yml +62 -0
  3. package/CHANGELOG.md +583 -0
  4. package/README.ja.md +42 -5
  5. package/README.md +400 -23
  6. package/bin/throughline.mjs +168 -4
  7. package/codex/skills/throughline/SKILL.md +157 -0
  8. package/codex/skills/throughline/agents/openai.yaml +7 -0
  9. package/docs/INHERITANCE_ON_CLEAR_ONLY.md +146 -0
  10. package/docs/L1_L2_L3_REDESIGN.md +415 -0
  11. package/docs/PUBLIC_RELEASE_PLAN.md +184 -0
  12. package/docs/THROUGHLINE_CODEX_DUAL_SUPPORT.md +249 -0
  13. package/docs/THROUGHLINE_CODEX_FIRST_ROADMAP.md +555 -0
  14. package/docs/THROUGHLINE_CODEX_MONITOR_IMPLEMENTATION_PLAN.md +220 -0
  15. package/docs/THROUGHLINE_CODEX_TRIM_IMPLEMENTATION_PLAN.md +528 -0
  16. package/docs/THROUGHLINE_CODEX_TRIM_ROLLBACK_FIX_PLAN.md +672 -0
  17. package/docs/archive/CONCEPT.md +476 -0
  18. package/docs/archive/EXPERIMENT.md +371 -0
  19. package/docs/archive/README.md +22 -0
  20. package/docs/archive/SESSION_LINKING_DESIGN.md +231 -0
  21. package/docs/archive/THROUGHLINE_NEXT_STEPS.md +134 -0
  22. package/docs/throughline-codex-trim-rollback-incident-report.md +306 -0
  23. package/docs/throughline-handoff-context.example.json +57 -0
  24. package/docs/throughline-rollback-context-trim-insight.md +455 -0
  25. package/package.json +6 -2
  26. package/src/cli/codex-capture.mjs +95 -0
  27. package/src/cli/codex-handoff-model-smoke.mjs +292 -0
  28. package/src/cli/codex-handoff-model-smoke.test.mjs +262 -0
  29. package/src/cli/codex-handoff-smoke.mjs +163 -0
  30. package/src/cli/codex-handoff-smoke.test.mjs +149 -0
  31. package/src/cli/codex-handoff-start.mjs +291 -0
  32. package/src/cli/codex-handoff-start.test.mjs +194 -0
  33. package/src/cli/codex-hook.mjs +276 -0
  34. package/src/cli/codex-hook.test.mjs +293 -0
  35. package/src/cli/codex-host-primitive-audit.mjs +110 -0
  36. package/src/cli/codex-host-primitive-audit.test.mjs +75 -0
  37. package/src/cli/codex-restore-smoke.mjs +357 -0
  38. package/src/cli/codex-restore-source-audit.mjs +304 -0
  39. package/src/cli/codex-resume.mjs +138 -0
  40. package/src/cli/codex-rollback-model-visible-smoke.mjs +373 -0
  41. package/src/cli/codex-rollback-model-visible-smoke.test.mjs +255 -0
  42. package/src/cli/codex-sidecar-diagnostics.mjs +48 -0
  43. package/src/cli/codex-sidecar-dry-run.mjs +85 -0
  44. package/src/cli/codex-summarize.mjs +224 -0
  45. package/src/cli/codex-threads.mjs +89 -0
  46. package/src/cli/codex-visibility-smoke.mjs +196 -0
  47. package/src/cli/codex-vscode-restore-smoke.mjs +226 -0
  48. package/src/cli/codex-vscode-rollback-smoke.mjs +114 -0
  49. package/src/cli/doctor.mjs +503 -1
  50. package/src/cli/doctor.test.mjs +542 -3
  51. package/src/cli/handoff-preview.mjs +78 -0
  52. package/src/cli/help.test.mjs +64 -0
  53. package/src/cli/install.mjs +227 -4
  54. package/src/cli/install.test.mjs +207 -4
  55. package/src/cli/trim.mjs +564 -0
  56. package/src/codex-app-server.mjs +1816 -0
  57. package/src/codex-app-server.test.mjs +512 -0
  58. package/src/codex-auto-refresh.mjs +194 -0
  59. package/src/codex-auto-refresh.test.mjs +182 -0
  60. package/src/codex-capture.mjs +235 -0
  61. package/src/codex-capture.test.mjs +393 -0
  62. package/src/codex-handoff-model-smoke.mjs +114 -0
  63. package/src/codex-handoff-model-smoke.test.mjs +89 -0
  64. package/src/codex-handoff-smoke.mjs +124 -0
  65. package/src/codex-handoff-smoke.test.mjs +103 -0
  66. package/src/codex-handoff.mjs +331 -0
  67. package/src/codex-handoff.test.mjs +220 -0
  68. package/src/codex-host-primitive-audit.mjs +374 -0
  69. package/src/codex-host-primitive-audit.test.mjs +208 -0
  70. package/src/codex-restore-smoke.test.mjs +639 -0
  71. package/src/codex-restore-source-audit.mjs +1348 -0
  72. package/src/codex-restore-source-audit.test.mjs +623 -0
  73. package/src/codex-resume.test.mjs +242 -0
  74. package/src/codex-rollout-memory.mjs +711 -0
  75. package/src/codex-rollout-memory.test.mjs +610 -0
  76. package/src/codex-sidecar-cli.test.mjs +75 -0
  77. package/src/codex-sidecar.mjs +246 -0
  78. package/src/codex-sidecar.test.mjs +172 -0
  79. package/src/codex-summarize.test.mjs +143 -0
  80. package/src/codex-thread-identity.mjs +23 -0
  81. package/src/codex-thread-index.mjs +173 -0
  82. package/src/codex-thread-index.test.mjs +164 -0
  83. package/src/codex-usage.mjs +110 -0
  84. package/src/codex-usage.test.mjs +140 -0
  85. package/src/codex-visibility-smoke.test.mjs +222 -0
  86. package/src/codex-vscode-restore-smoke.mjs +206 -0
  87. package/src/codex-vscode-restore-smoke.test.mjs +325 -0
  88. package/src/codex-vscode-rollback-smoke.mjs +90 -0
  89. package/src/codex-vscode-rollback-smoke.test.mjs +290 -0
  90. package/src/db-schema.test.mjs +97 -0
  91. package/src/haiku-summarizer.mjs +267 -26
  92. package/src/haiku-summarizer.test.mjs +282 -0
  93. package/src/handoff-preview.test.mjs +108 -0
  94. package/src/handoff-record.mjs +294 -0
  95. package/src/handoff-record.test.mjs +226 -0
  96. package/src/hook-entrypoints.test.mjs +326 -0
  97. package/src/package-files.test.mjs +19 -0
  98. package/src/prompt-submit.mjs +9 -6
  99. package/src/resume-context.mjs +44 -140
  100. package/src/resume-context.test.mjs +172 -0
  101. package/src/session-start.mjs +8 -5
  102. package/src/state-file.mjs +50 -6
  103. package/src/state-file.test.mjs +50 -0
  104. package/src/token-monitor.mjs +14 -10
  105. package/src/token-monitor.test.mjs +27 -0
  106. package/src/trim-cli.test.mjs +1584 -0
  107. package/src/trim-model.mjs +584 -0
  108. package/src/trim-model.test.mjs +568 -0
  109. package/src/turn-processor.mjs +17 -10
  110. package/src/vscode-task.mjs +94 -6
  111. package/src/vscode-task.test.mjs +186 -6
@@ -19,6 +19,7 @@ import { join, dirname, isAbsolute } from 'node:path';
19
19
 
20
20
  const MONITOR_LABEL = 'Throughline Monitor';
21
21
  const JSONC_MARKER_FILENAME = '.throughline-jsonc-noted';
22
+ const GITIGNORE_MARKER_FILENAME = '.throughline-gitignore-noted';
22
23
 
23
24
  /**
24
25
  * VSCode 系エディタが動いているかを env から推定する。
@@ -238,14 +239,98 @@ export function buildSetupNotice(action) {
238
239
  return null;
239
240
  }
240
241
 
241
- function emitSetupNotice(action) {
242
+ function shouldEmitNotices(env) {
243
+ return env.THROUGHLINE_SUPPRESS_VSCODE_NOTICES !== '1';
244
+ }
245
+
246
+ function emitSetupNotice(action, env) {
247
+ if (!shouldEmitNotices(env)) return;
242
248
  const text = buildSetupNotice(action);
243
249
  if (text) process.stdout.write(text);
244
250
  }
245
251
 
246
- function emitJsoncGuidanceOnce(vscodeDir) {
252
+ /**
253
+ * `.vscode/tasks.json` が gitignore 推奨対象か判定する。
254
+ *
255
+ * 推奨条件:
256
+ * - cwd が git リポジトリ (`.git/` がある) かつ
257
+ * - `.gitignore` に `.vscode/tasks.json` 相当のエントリが無い
258
+ *
259
+ * 単独プロジェクト (.git なし) は判定対象外 (false を返す)。共有しないので
260
+ * gitignore の必要性そのものが無いため。
261
+ *
262
+ * @param {string} cwd
263
+ * @returns {boolean}
264
+ */
265
+ export function shouldRecommendGitignore(cwd) {
266
+ const gitDir = join(cwd, '.git');
267
+ if (!existsSync(gitDir)) return false;
268
+
269
+ const gitignorePath = join(cwd, '.gitignore');
270
+ if (!existsSync(gitignorePath)) return true;
271
+
272
+ const content = readFileSync(gitignorePath, 'utf8');
273
+ const lines = content.split(/\r?\n/);
274
+ for (const raw of lines) {
275
+ const line = raw.trim();
276
+ if (!line || line.startsWith('#')) continue;
277
+ // 否定 (!) は除外しない: "!.vscode/tasks.json" は明示的に追跡したい意図なので推奨を出すべき
278
+ if (line.startsWith('!')) continue;
279
+ // .vscode 全体除外 / tasks.json 単独除外 / .vscode/* どれかを含むか
280
+ if (line === '.vscode' || line === '.vscode/' || line === '/.vscode' || line === '/.vscode/') return false;
281
+ if (line === '.vscode/*' || line === '.vscode/**') return false;
282
+ if (line === '.vscode/tasks.json' || line === '/.vscode/tasks.json') return false;
283
+ }
284
+ return true;
285
+ }
286
+
287
+ function emitGitignoreRecommendationOnce(vscodeDir, env) {
288
+ const markerPath = join(vscodeDir, GITIGNORE_MARKER_FILENAME);
289
+ if (existsSync(markerPath)) return;
290
+ if (!shouldEmitNotices(env)) {
291
+ try {
292
+ writeFileSync(markerPath, `${new Date().toISOString()}\n`);
293
+ } catch (err) {
294
+ const msg = err instanceof Error ? err.message : 'unknown';
295
+ process.stderr.write(`[throughline] failed to write gitignore marker: ${msg}\n`);
296
+ }
297
+ return;
298
+ }
299
+ process.stdout.write(
300
+ '<system-reminder>\n' +
301
+ 'Throughline ヒント: .vscode/tasks.json には現環境の絶対パス (node 実行ファイル / throughline.mjs)\n' +
302
+ 'が書き込まれます。共有リポジトリで commit すると別 OS / 別 install 場所のメンバーで起動失敗の\n' +
303
+ '原因になるため、.gitignore に以下のいずれかを追加することを推奨します:\n' +
304
+ '\n' +
305
+ ' .vscode/tasks.json # tasks.json だけ除外 (settings.json 等は共有可)\n' +
306
+ ' .vscode/ # .vscode 配下を全部除外\n' +
307
+ '\n' +
308
+ 'なお Throughline v0.3.23+ は別環境の絶対パスが焼き込まれた tasks.json を\n' +
309
+ '自動修復するため、commit してしまっても致命的ではありません。あくまで推奨です。\n' +
310
+ '\n' +
311
+ 'この通知はプロジェクトごとに 1 度だけ流れます。必ずユーザー(あなたの対話相手)に日本語でそのまま伝えてください。\n' +
312
+ '</system-reminder>\n'
313
+ );
314
+ try {
315
+ writeFileSync(markerPath, `${new Date().toISOString()}\n`);
316
+ } catch (err) {
317
+ const msg = err instanceof Error ? err.message : 'unknown';
318
+ process.stderr.write(`[throughline] failed to write gitignore marker: ${msg}\n`);
319
+ }
320
+ }
321
+
322
+ function emitJsoncGuidanceOnce(vscodeDir, env) {
247
323
  const markerPath = join(vscodeDir, JSONC_MARKER_FILENAME);
248
324
  if (existsSync(markerPath)) return;
325
+ if (!shouldEmitNotices(env)) {
326
+ try {
327
+ writeFileSync(markerPath, `${new Date().toISOString()}\n`);
328
+ } catch (err) {
329
+ const msg = err instanceof Error ? err.message : 'unknown';
330
+ process.stderr.write(`[throughline] failed to write JSONC marker: ${msg}\n`);
331
+ }
332
+ return;
333
+ }
249
334
  process.stderr.write(
250
335
  `[throughline] .vscode/tasks.json contains JSONC features (comments or trailing commas). ` +
251
336
  `Auto-edit is unsafe on this file — add the Throughline Monitor task manually. ` +
@@ -301,14 +386,15 @@ export function ensureMonitorTaskFile(opts = {}) {
301
386
  if (!existsSync(vscodeDir)) mkdirSync(vscodeDir, { recursive: true });
302
387
  const obj = { version: '2.0.0', tasks: [buildMonitorTask(bin)] };
303
388
  atomicWrite(tasksPath, JSON.stringify(obj, null, 2) + '\n');
304
- emitSetupNotice('created');
389
+ emitSetupNotice('created', env);
390
+ if (shouldRecommendGitignore(cwd)) emitGitignoreRecommendationOnce(vscodeDir, env);
305
391
  return { action: 'created', path: tasksPath };
306
392
  }
307
393
 
308
394
  const text = readFileSync(tasksPath, 'utf8');
309
395
 
310
396
  if (detectJsoncFeatures(text)) {
311
- emitJsoncGuidanceOnce(vscodeDir);
397
+ emitJsoncGuidanceOnce(vscodeDir, env);
312
398
  return { action: 'skipped', reason: 'jsonc_unsupported', path: tasksPath };
313
399
  }
314
400
 
@@ -335,7 +421,8 @@ export function ensureMonitorTaskFile(opts = {}) {
335
421
  nextTasks[existingIdx] = repaired;
336
422
  const nextObj = { ...obj, version: obj.version ?? '2.0.0', tasks: nextTasks };
337
423
  atomicWrite(tasksPath, JSON.stringify(nextObj, null, indent) + '\n');
338
- emitSetupNotice('repaired');
424
+ emitSetupNotice('repaired', env);
425
+ if (shouldRecommendGitignore(cwd)) emitGitignoreRecommendationOnce(vscodeDir, env);
339
426
  return { action: 'repaired', path: tasksPath };
340
427
  }
341
428
  return { action: 'already_present', path: tasksPath };
@@ -348,6 +435,7 @@ export function ensureMonitorTaskFile(opts = {}) {
348
435
  tasks: [...(Array.isArray(obj.tasks) ? obj.tasks : []), buildMonitorTask(bin)],
349
436
  };
350
437
  atomicWrite(tasksPath, JSON.stringify(nextObj, null, indent) + '\n');
351
- emitSetupNotice('merged');
438
+ emitSetupNotice('merged', env);
439
+ if (shouldRecommendGitignore(cwd)) emitGitignoreRecommendationOnce(vscodeDir, env);
352
440
  return { action: 'merged', path: tasksPath };
353
441
  }
@@ -13,9 +13,16 @@ import {
13
13
  isMonitorTaskBroken,
14
14
  buildMonitorTask,
15
15
  buildSetupNotice,
16
+ shouldRecommendGitignore,
16
17
  } from './vscode-task.mjs';
17
18
 
18
- const VSCODE_ENV = { TERM_PROGRAM: 'vscode' };
19
+ const VSCODE_ENV = {
20
+ TERM_PROGRAM: 'vscode',
21
+ THROUGHLINE_SUPPRESS_VSCODE_NOTICES: '1',
22
+ };
23
+ // Production notices are Claude-facing additional context. Tests keep them
24
+ // silent by default and opt in only when asserting notice text.
25
+ const VSCODE_NOTICE_ENV = { TERM_PROGRAM: 'vscode' };
19
26
  // 実在する絶対パスを使う。`isMonitorTaskBroken` が「絶対パス + 非存在」で broken 判定するので、
20
27
  // 架空パスを使うと意図せず repaired ブランチに落ちてしまう。
21
28
  const FAKE_BIN = process.execPath;
@@ -435,7 +442,11 @@ test('ensureMonitorTaskFile: already_present when command references throughline
435
442
  {
436
443
  label: 'My Custom Monitor',
437
444
  type: 'process',
438
- command: '/usr/bin/node',
445
+ // 実在する絶対パスを使う。`/usr/bin/node` は WSL2 では実在するが
446
+ // CI runner (Linux/macOS は /opt/hostedtoolcache, Windows は別) では
447
+ // 存在しないので isMonitorTaskBroken が true になり repaired ブランチに落ちる。
448
+ // process.execPath なら「いま走らせている node 自身の絶対パス」なので必ず実在する。
449
+ command: process.execPath,
439
450
  // 相対パスにして broken 判定を避ける(このテストは「label renamed でも検出できるか」だけが論点)
440
451
  args: ['./throughline.mjs', 'monitor'],
441
452
  },
@@ -481,6 +492,175 @@ test('ensureMonitorTaskFile: second call is idempotent (already_present after cr
481
492
  }
482
493
  });
483
494
 
495
+ // --- shouldRecommendGitignore ---
496
+
497
+ test('shouldRecommendGitignore: false when not a git repo (.git missing)', () => {
498
+ const { dir, cleanup } = mkTmpCwd();
499
+ try {
500
+ assert.equal(shouldRecommendGitignore(dir), false);
501
+ } finally {
502
+ cleanup();
503
+ }
504
+ });
505
+
506
+ test('shouldRecommendGitignore: true when git repo has no .gitignore', () => {
507
+ const { dir, cleanup } = mkTmpCwd();
508
+ try {
509
+ mkdirSync(join(dir, '.git'));
510
+ assert.equal(shouldRecommendGitignore(dir), true);
511
+ } finally {
512
+ cleanup();
513
+ }
514
+ });
515
+
516
+ test('shouldRecommendGitignore: true when .gitignore does not list .vscode/tasks.json', () => {
517
+ const { dir, cleanup } = mkTmpCwd();
518
+ try {
519
+ mkdirSync(join(dir, '.git'));
520
+ writeFileSync(join(dir, '.gitignore'), 'node_modules/\n*.log\n');
521
+ assert.equal(shouldRecommendGitignore(dir), true);
522
+ } finally {
523
+ cleanup();
524
+ }
525
+ });
526
+
527
+ test('shouldRecommendGitignore: false when .gitignore has .vscode/tasks.json', () => {
528
+ const { dir, cleanup } = mkTmpCwd();
529
+ try {
530
+ mkdirSync(join(dir, '.git'));
531
+ writeFileSync(join(dir, '.gitignore'), 'node_modules/\n.vscode/tasks.json\n');
532
+ assert.equal(shouldRecommendGitignore(dir), false);
533
+ } finally {
534
+ cleanup();
535
+ }
536
+ });
537
+
538
+ test('shouldRecommendGitignore: false when .gitignore has .vscode/ (whole dir)', () => {
539
+ const { dir, cleanup } = mkTmpCwd();
540
+ try {
541
+ mkdirSync(join(dir, '.git'));
542
+ writeFileSync(join(dir, '.gitignore'), '.vscode/\n');
543
+ assert.equal(shouldRecommendGitignore(dir), false);
544
+ } finally {
545
+ cleanup();
546
+ }
547
+ });
548
+
549
+ test('shouldRecommendGitignore: false when .gitignore has .vscode (no slash)', () => {
550
+ const { dir, cleanup } = mkTmpCwd();
551
+ try {
552
+ mkdirSync(join(dir, '.git'));
553
+ writeFileSync(join(dir, '.gitignore'), '.vscode\n');
554
+ assert.equal(shouldRecommendGitignore(dir), false);
555
+ } finally {
556
+ cleanup();
557
+ }
558
+ });
559
+
560
+ test('shouldRecommendGitignore: ignores comments and negation lines', () => {
561
+ const { dir, cleanup } = mkTmpCwd();
562
+ try {
563
+ mkdirSync(join(dir, '.git'));
564
+ // 否定パターンは「除外しない」意図なので、推奨は引き続き出す
565
+ writeFileSync(join(dir, '.gitignore'), '# comment\n!.vscode/tasks.json\n');
566
+ assert.equal(shouldRecommendGitignore(dir), true);
567
+ } finally {
568
+ cleanup();
569
+ }
570
+ });
571
+
572
+ // --- ensureMonitorTaskFile: gitignore recommendation notice ---
573
+
574
+ test('ensureMonitorTaskFile: created emits gitignore recommendation when .git exists and no .gitignore entry', () => {
575
+ const { dir, cleanup } = mkTmpCwd();
576
+ const captured = [];
577
+ const origWrite = process.stdout.write.bind(process.stdout);
578
+ process.stdout.write = (chunk) => {
579
+ captured.push(typeof chunk === 'string' ? chunk : chunk.toString('utf8'));
580
+ return true;
581
+ };
582
+ try {
583
+ mkdirSync(join(dir, '.git'));
584
+ const result = ensureMonitorTaskFile({
585
+ cwd: dir,
586
+ env: VSCODE_NOTICE_ENV,
587
+ throughlineBin: FAKE_BIN,
588
+ });
589
+ assert.equal(result.action, 'created');
590
+ } finally {
591
+ process.stdout.write = origWrite;
592
+ cleanup();
593
+ }
594
+ const joined = captured.join('');
595
+ assert.ok(joined.includes('.gitignore'), 'should emit gitignore recommendation');
596
+ assert.ok(joined.includes('Reload Window'), 'should still emit setup notice');
597
+ });
598
+
599
+ test('ensureMonitorTaskFile: created does NOT emit gitignore recommendation when not a git repo', () => {
600
+ const { dir, cleanup } = mkTmpCwd();
601
+ const captured = [];
602
+ const origWrite = process.stdout.write.bind(process.stdout);
603
+ process.stdout.write = (chunk) => {
604
+ captured.push(typeof chunk === 'string' ? chunk : chunk.toString('utf8'));
605
+ return true;
606
+ };
607
+ try {
608
+ const result = ensureMonitorTaskFile({
609
+ cwd: dir,
610
+ env: VSCODE_NOTICE_ENV,
611
+ throughlineBin: FAKE_BIN,
612
+ });
613
+ assert.equal(result.action, 'created');
614
+ } finally {
615
+ process.stdout.write = origWrite;
616
+ cleanup();
617
+ }
618
+ const joined = captured.join('');
619
+ assert.ok(!joined.includes('gitignore'), 'should not mention gitignore for non-git dirs');
620
+ });
621
+
622
+ test('ensureMonitorTaskFile: gitignore recommendation is emitted only once per project', () => {
623
+ const { dir, cleanup } = mkTmpCwd();
624
+ try {
625
+ mkdirSync(join(dir, '.git'));
626
+
627
+ const captured = [];
628
+ const origWrite = process.stdout.write.bind(process.stdout);
629
+ process.stdout.write = (chunk) => {
630
+ captured.push(typeof chunk === 'string' ? chunk : chunk.toString('utf8'));
631
+ return true;
632
+ };
633
+ try {
634
+ // 1 回目: created → gitignore 推奨が出る
635
+ const r1 = ensureMonitorTaskFile({
636
+ cwd: dir,
637
+ env: VSCODE_NOTICE_ENV,
638
+ throughlineBin: FAKE_BIN,
639
+ });
640
+ assert.equal(r1.action, 'created');
641
+ const firstCount = captured.filter((s) => s.includes('gitignore')).length;
642
+ assert.equal(firstCount, 1);
643
+
644
+ // tasks.json を一度消して再 created 状況を作る
645
+ // (実運用では already_present になるので現実的ではないが、marker の効きを見る)
646
+ const tasksPath = join(dir, '.vscode', 'tasks.json');
647
+ rmSync(tasksPath);
648
+ const r2 = ensureMonitorTaskFile({
649
+ cwd: dir,
650
+ env: VSCODE_ENV,
651
+ throughlineBin: FAKE_BIN,
652
+ });
653
+ assert.equal(r2.action, 'created');
654
+ const secondCount = captured.filter((s) => s.includes('gitignore')).length;
655
+ assert.equal(secondCount, 1, 'marker file should suppress 2nd recommendation');
656
+ } finally {
657
+ process.stdout.write = origWrite;
658
+ }
659
+ } finally {
660
+ cleanup();
661
+ }
662
+ });
663
+
484
664
  // --- ensureMonitorTaskFile: cross-environment repair (地雷 4) ---
485
665
 
486
666
  test('ensureMonitorTaskFile: repaired when existing task points to non-existent absolute paths', () => {
@@ -621,7 +801,7 @@ test('ensureMonitorTaskFile: repaired emits notice on stdout', () => {
621
801
 
622
802
  const result = ensureMonitorTaskFile({
623
803
  cwd: dir,
624
- env: VSCODE_ENV,
804
+ env: VSCODE_NOTICE_ENV,
625
805
  throughlineBin: FAKE_BIN,
626
806
  });
627
807
  assert.equal(result.action, 'repaired');
@@ -697,7 +877,7 @@ test('ensureMonitorTaskFile: jsonc_unsupported marker suppresses stderr on 2nd c
697
877
  try {
698
878
  const r1 = ensureMonitorTaskFile({
699
879
  cwd: dir,
700
- env: VSCODE_ENV,
880
+ env: VSCODE_NOTICE_ENV,
701
881
  throughlineBin: FAKE_BIN,
702
882
  });
703
883
  assert.equal(r1.action, 'skipped');
@@ -786,13 +966,13 @@ test('buildSetupNotice: ensureMonitorTaskFile writes notice to stdout on first c
786
966
  try {
787
967
  const r1 = ensureMonitorTaskFile({
788
968
  cwd: dir,
789
- env: VSCODE_ENV,
969
+ env: VSCODE_NOTICE_ENV,
790
970
  throughlineBin: FAKE_BIN,
791
971
  });
792
972
  assert.equal(r1.action, 'created');
793
973
  const r2 = ensureMonitorTaskFile({
794
974
  cwd: dir,
795
- env: VSCODE_ENV,
975
+ env: VSCODE_NOTICE_ENV,
796
976
  throughlineBin: FAKE_BIN,
797
977
  });
798
978
  assert.equal(r2.action, 'already_present');