throughline 0.10.2 → 0.10.5

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 (68) hide show
  1. package/CHANGELOG.md +88 -27
  2. package/README.ja.md +83 -49
  3. package/README.md +106 -78
  4. package/bin/throughline.mjs +32 -13
  5. package/docs/00_overview.md +56 -42
  6. package/docs/01_l1_l2_l3_redesign.md +1 -1
  7. package/docs/02_clear_auto_handoff_plan.md +39 -333
  8. package/docs/04_public_release_plan.md +73 -190
  9. package/docs/05_codex_first_roadmap.md +4 -4
  10. package/docs/06_codex_trim_rollback_fix_plan.md +1 -1
  11. package/docs/08_codex_dual_support.md +1 -1
  12. package/docs/09_rollback_context_trim_insight.md +1 -1
  13. package/docs/12_desktop_clear_handoff_plan.md +6 -213
  14. package/docs/15_windows_ci_release_latency_plan.md +6 -87
  15. package/docs/16_readonly_handoff_context_plan.md +7 -38
  16. package/docs/adr/0005-observer-read-pagination.md +1 -1
  17. package/docs/adr/0014-two-phase-handoff-ghost-baton.md +1 -1
  18. package/docs/adr/0019-product-owned-database-migration-acceptance.md +1 -1
  19. package/docs/adr/0021-grok-host-capture.md +1 -1
  20. package/docs/adr/0022-cursor-host-capture.md +39 -0
  21. package/docs/archive/02_clear_auto_handoff_plan.md +350 -0
  22. package/docs/{03_inheritance_on_clear_only.md → archive/03_inheritance_on_clear_only.md} +22 -22
  23. package/docs/{07_codex_trim_implementation_plan.md → archive/07_codex_trim_implementation_plan.md} +8 -8
  24. package/docs/{10_transcript_injection_plan.md → archive/10_transcript_injection_plan.md} +12 -12
  25. package/docs/archive/12_desktop_clear_handoff_plan.md +218 -0
  26. package/docs/{14_observer_completed_turn_feed_plan.md → archive/14_observer_completed_turn_feed_plan.md} +7 -7
  27. package/docs/archive/15_windows_ci_release_latency_plan.md +89 -0
  28. package/docs/archive/16_readonly_handoff_context_plan.md +40 -0
  29. package/docs/archive/README.md +28 -15
  30. package/docs/archive/plan_grok-successor-launch.md +99 -0
  31. package/docs/archive/room-log_throughline_20260830-155052.md +285 -0
  32. package/docs/plan_grok-successor-launch.md +6 -97
  33. package/package.json +19 -11
  34. package/rag/INDEX.md +2 -2
  35. package/src/baton.mjs +11 -9
  36. package/src/cli/handoff-context.test.mjs +36 -0
  37. package/src/cli/help.test.mjs +5 -0
  38. package/src/cli/install.mjs +91 -0
  39. package/src/cli/install.test.mjs +57 -0
  40. package/src/cli/runtime-errors.mjs +9 -3
  41. package/src/cli/runtime-errors.test.mjs +13 -13
  42. package/src/cli/self-update.mjs +402 -0
  43. package/src/cli/self-update.test.mjs +525 -0
  44. package/src/db.mjs +1 -1
  45. package/src/docs-contract.test.mjs +153 -0
  46. package/src/hosts/claude.mjs +1 -0
  47. package/src/hosts/codex.mjs +1 -0
  48. package/src/hosts/cursor.mjs +128 -0
  49. package/src/hosts/cursor.test.mjs +104 -0
  50. package/src/hosts/grok.mjs +1 -0
  51. package/src/hosts/identity.mjs +19 -2
  52. package/src/hosts/identity.test.mjs +27 -4
  53. package/src/hosts/index.mjs +11 -2
  54. package/src/product-ci-contract.test.mjs +14 -0
  55. package/src/prompt-submit.mjs +8 -10
  56. package/src/resume-context.mjs +4 -4
  57. package/src/runtime-error-hook.test.mjs +4 -6
  58. package/src/runtime-error-store.mjs +40 -18
  59. package/src/runtime-error-store.test.mjs +53 -26
  60. package/src/session-merger.mjs +16 -7
  61. package/src/session-merger.test.mjs +27 -0
  62. package/src/session-start.mjs +24 -1
  63. package/src/spike-transcript-writer.mjs +1 -1
  64. package/src/transcript-reader-cursor.test.mjs +43 -0
  65. package/src/transcript-reader.mjs +14 -7
  66. /package/docs/{11_codex_monitor_implementation_plan.md → archive/11_codex_monitor_implementation_plan.md} +0 -0
  67. /package/docs/{13_native_factory_diagnostics_plan.md → archive/13_native_factory_diagnostics_plan.md} +0 -0
  68. /package/docs/{BUGHUB_RUNTIME_ERROR_STORE_PLAN.md → archive/BUGHUB_RUNTIME_ERROR_STORE_PLAN.md} +0 -0
@@ -11,6 +11,7 @@
11
11
  * script path で登録する。
12
12
  * Grok-facing hook も Desktop の GUI PATH に throughline が無いため、同じ絶対
13
13
  * node + CLI script path で ~/.grok/hooks/throughline.json に書く。
14
+ * Cursor-facing hook は ~/.cursor/hooks.json へ upsert する(工場 hook は残す)。
14
15
  */
15
16
 
16
17
  import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, copyFileSync, unlinkSync, rmSync, realpathSync } from 'node:fs';
@@ -27,6 +28,7 @@ const CODEX_SKILL_NAMES = ['throughline'];
27
28
  const CODEX_HOOKS_RELATIVE_PATH = ['.codex', 'hooks.json'];
28
29
  const CODEX_CONFIG_RELATIVE_PATH = ['.codex', 'config.toml'];
29
30
  const GROK_HOOKS_RELATIVE_PATH = ['.grok', 'hooks', 'throughline.json'];
31
+ const CURSOR_HOOKS_RELATIVE_PATH = ['.cursor', 'hooks.json'];
30
32
 
31
33
  // Throughline が管理する hook コマンド一覧
32
34
  // schema v4 以降: PostToolUse (capture-tool) は廃止。Stop 内で L2/L3 を一括処理する。
@@ -297,6 +299,16 @@ export function buildGrokHookCommand(subcommand, {
297
299
  return `${quoteCommandPath(nodePath)} ${quoteCommandPath(cliScriptPath)} ${subcommand}`;
298
300
  }
299
301
 
302
+ export function buildCursorHookCommand(subcommand, options = {}) {
303
+ return buildGrokHookCommand(subcommand, options);
304
+ }
305
+
306
+ export function isThroughlineCursorHookCommand(command) {
307
+ return typeof command === 'string'
308
+ && command.includes('throughline.mjs')
309
+ && /\b(session-start|prompt-submit|process-turn)\b/.test(command);
310
+ }
311
+
300
312
  export function createGrokHooksFile(options = {}) {
301
313
  return {
302
314
  hooks: {
@@ -334,6 +346,74 @@ function uninstallGrokHooks() {
334
346
  return { hooksPath, removed: 1 };
335
347
  }
336
348
 
349
+ function resolveCursorHooksPath() {
350
+ return join(homedir(), ...CURSOR_HOOKS_RELATIVE_PATH);
351
+ }
352
+
353
+ export function createCursorHookEntries(options = {}) {
354
+ return {
355
+ sessionStart: [
356
+ { command: buildCursorHookCommand('session-start', options), timeout: 10 },
357
+ ],
358
+ beforeSubmitPrompt: [
359
+ { command: buildCursorHookCommand('prompt-submit', options), timeout: 30 },
360
+ ],
361
+ stop: [
362
+ { command: buildCursorHookCommand('process-turn', options), timeout: 300 },
363
+ ],
364
+ };
365
+ }
366
+
367
+ function readCursorHooksFile(hooksPath) {
368
+ if (!existsSync(hooksPath)) return { version: 1, hooks: {} };
369
+ const parsed = JSON.parse(readFileSync(hooksPath, 'utf8'));
370
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
371
+ throw new Error(`${hooksPath} は object である必要があります`);
372
+ }
373
+ if (parsed.hooks == null) parsed.hooks = {};
374
+ if (typeof parsed.hooks !== 'object' || Array.isArray(parsed.hooks)) {
375
+ throw new Error(`${hooksPath} の hooks は object である必要があります`);
376
+ }
377
+ return parsed;
378
+ }
379
+
380
+ function installCursorHooks() {
381
+ const hooksPath = resolveCursorHooksPath();
382
+ mkdirSync(dirname(hooksPath), { recursive: true });
383
+ const current = readCursorHooksFile(hooksPath);
384
+ current.version = 1;
385
+ const wanted = createCursorHookEntries();
386
+ for (const [event, entries] of Object.entries(wanted)) {
387
+ const existing = Array.isArray(current.hooks[event]) ? current.hooks[event] : [];
388
+ const kept = existing.filter((entry) => !isThroughlineCursorHookCommand(entry?.command));
389
+ current.hooks[event] = [...kept, ...entries];
390
+ }
391
+ writeFileSync(hooksPath, `${JSON.stringify(current, null, 2)}\n`);
392
+ return { hooksPath };
393
+ }
394
+
395
+ function uninstallCursorHooks() {
396
+ const hooksPath = resolveCursorHooksPath();
397
+ if (!existsSync(hooksPath)) return { hooksPath, removed: 0 };
398
+ const current = readCursorHooksFile(hooksPath);
399
+ let removed = 0;
400
+ for (const [event, list] of Object.entries(current.hooks ?? {})) {
401
+ if (!Array.isArray(list)) continue;
402
+ const kept = list.filter((entry) => {
403
+ if (isThroughlineCursorHookCommand(entry?.command)) {
404
+ removed += 1;
405
+ return false;
406
+ }
407
+ return true;
408
+ });
409
+ if (kept.length > 0) current.hooks[event] = kept;
410
+ else delete current.hooks[event];
411
+ }
412
+ if (removed === 0) return { hooksPath, removed: 0 };
413
+ writeFileSync(hooksPath, `${JSON.stringify(current, null, 2)}\n`);
414
+ return { hooksPath, removed };
415
+ }
416
+
337
417
  function installSlashCommands(commandsDir) {
338
418
  if (!existsSync(SLASH_COMMANDS_SRC)) {
339
419
  return { installed: [], skipped: 'source-missing' };
@@ -589,6 +669,7 @@ export async function run(args = []) {
589
669
  const removedCommands = uninstallSlashCommands(commandsDir);
590
670
  const codex = args.includes('--project') ? null : uninstallCodexHooks();
591
671
  const grok = args.includes('--project') ? null : uninstallGrokHooks();
672
+ const cursor = args.includes('--project') ? null : uninstallCursorHooks();
592
673
  const removedCodexSkills = args.includes('--project') ? [] : uninstallCodexSkills(codexSkillsDir);
593
674
  console.log('Throughline hooks を削除しました。');
594
675
  console.log(` ${settingsPath}`);
@@ -601,6 +682,9 @@ export async function run(args = []) {
601
682
  if (grok?.removed > 0) {
602
683
  console.log(` Grok hooks 削除: ${grok.removed} (${grok.hooksPath})`);
603
684
  }
685
+ if (cursor?.removed > 0) {
686
+ console.log(` Cursor hooks 削除: ${cursor.removed} (${cursor.hooksPath})`);
687
+ }
604
688
  if (removedCodexSkills.length > 0) {
605
689
  console.log(` Codex skills 削除: ${removedCodexSkills.join(', ')} (${codexSkillsDir})`);
606
690
  }
@@ -624,6 +708,7 @@ export async function run(args = []) {
624
708
  const { installed: installedCommands, skipped } = installSlashCommands(commandsDir);
625
709
  const codex = args.includes('--project') ? null : installCodexHooks();
626
710
  const grok = args.includes('--project') ? null : installGrokHooks();
711
+ const cursor = args.includes('--project') ? null : installCursorHooks();
627
712
  const codexSkills = args.includes('--project') ? { installed: [], skipped: null } : installCodexSkills(codexSkillsDir);
628
713
  const monitorTask = ensureMonitorTaskFile({
629
714
  cwd: process.cwd(),
@@ -643,6 +728,9 @@ export async function run(args = []) {
643
728
  if (grok) {
644
729
  console.log(` ${grok.hooksPath}`);
645
730
  }
731
+ if (cursor) {
732
+ console.log(` ${cursor.hooksPath}`);
733
+ }
646
734
  console.log('');
647
735
  console.log('有効な hooks:');
648
736
  console.log(' SessionStart → throughline session-start (セッション記録・バトン消費・引き継ぎ注入)');
@@ -656,6 +744,9 @@ export async function run(args = []) {
656
744
  if (grok) {
657
745
  console.log(' Grok SessionStart / UserPromptSubmit / Stop → ~/.grok/hooks/throughline.json');
658
746
  }
747
+ if (cursor) {
748
+ console.log(' Cursor sessionStart / beforeSubmitPrompt / stop → ~/.cursor/hooks.json(工場hookは残す)');
749
+ }
659
750
  console.log('');
660
751
  if (installedCommands.length > 0) {
661
752
  console.log(`slash commands を配置しました: ${installedCommands.map(n => '/' + n.replace(/\.md$/, '')).join(', ')}`);
@@ -10,8 +10,10 @@ import {
10
10
  buildCodexUserPromptSubmitHookCommand,
11
11
  buildGrokHookCommand,
12
12
  createGrokHooksFile,
13
+ createCursorHookEntries,
13
14
  isEquivalentCodexHookCommand,
14
15
  isThroughlineCodexHookCommand,
16
+ isThroughlineCursorHookCommand,
15
17
  parseCodexHookCommand,
16
18
  resolveCodexHookNodePath,
17
19
  run,
@@ -92,6 +94,28 @@ test('global install copies Throughline slash commands to ~/.claude/commands/',
92
94
  assert.match(command, /throughline\.mjs/);
93
95
  assert.doesNotMatch(command, /^throughline /);
94
96
  }
97
+ const cursorHooks = JSON.parse(readFileSync(join(home.dir, '.cursor', 'hooks.json'), 'utf8'));
98
+ assert.equal(cursorHooks.version, 1);
99
+ assert.equal(
100
+ cursorHooks.hooks.sessionStart[0].command,
101
+ createCursorHookEntries().sessionStart[0].command,
102
+ );
103
+ assert.equal(
104
+ cursorHooks.hooks.beforeSubmitPrompt[0].command,
105
+ createCursorHookEntries().beforeSubmitPrompt[0].command,
106
+ );
107
+ assert.equal(
108
+ cursorHooks.hooks.stop[0].command,
109
+ createCursorHookEntries().stop[0].command,
110
+ );
111
+ for (const command of [
112
+ cursorHooks.hooks.sessionStart[0].command,
113
+ cursorHooks.hooks.beforeSubmitPrompt[0].command,
114
+ cursorHooks.hooks.stop[0].command,
115
+ ]) {
116
+ assert.equal(isThroughlineCursorHookCommand(command), true);
117
+ assert.doesNotMatch(command, /^throughline /);
118
+ }
95
119
  } finally {
96
120
  unsilence();
97
121
  home.restore();
@@ -192,6 +216,39 @@ test('Grok hook commands are absolute node + throughline.mjs on every platform',
192
216
  assert.equal(file.hooks.Stop[0].hooks[0].async, true);
193
217
  });
194
218
 
219
+ test('Cursor install upserts product hooks and keeps factory commands', async () => {
220
+ const home = makeTempHome();
221
+ if (home.resolved !== home.dir) {
222
+ home.restore();
223
+ return;
224
+ }
225
+ const unsilence = silence();
226
+ try {
227
+ mkdirSync(join(home.dir, '.cursor'), { recursive: true });
228
+ writeFileSync(join(home.dir, '.cursor', 'hooks.json'), `${JSON.stringify({
229
+ version: 1,
230
+ hooks: {
231
+ sessionStart: [{ command: '/tmp/cursor-todo-gate-hook session-start', timeout: 10 }],
232
+ },
233
+ }, null, 2)}\n`);
234
+ await run([]);
235
+ const installed = JSON.parse(readFileSync(join(home.dir, '.cursor', 'hooks.json'), 'utf8'));
236
+ assert.equal(installed.hooks.sessionStart[0].command, '/tmp/cursor-todo-gate-hook session-start');
237
+ assert.equal(isThroughlineCursorHookCommand(installed.hooks.sessionStart[1].command), true);
238
+ assert.equal(isThroughlineCursorHookCommand(installed.hooks.beforeSubmitPrompt[0].command), true);
239
+ assert.equal(isThroughlineCursorHookCommand(installed.hooks.stop[0].command), true);
240
+ await run(['--uninstall']);
241
+ const after = JSON.parse(readFileSync(join(home.dir, '.cursor', 'hooks.json'), 'utf8'));
242
+ assert.equal(after.hooks.sessionStart.length, 1);
243
+ assert.equal(after.hooks.sessionStart[0].command, '/tmp/cursor-todo-gate-hook session-start');
244
+ assert.equal(after.hooks.beforeSubmitPrompt, undefined);
245
+ assert.equal(after.hooks.stop, undefined);
246
+ } finally {
247
+ unsilence();
248
+ home.restore();
249
+ }
250
+ });
251
+
195
252
  test('Codex hook builders use the PowerShell call operator on Windows only', () => {
196
253
  const options = {
197
254
  nodePath: String.raw`C:\Program Files\nodejs\node.exe`,
@@ -5,13 +5,14 @@ import {
5
5
  readRuntimeErrorSnapshot,
6
6
  reopenRuntimeError,
7
7
  resolveRuntimeError,
8
+ setRuntimeErrorCollectionEnabled,
8
9
  } from '../runtime-error-store.mjs';
9
10
 
10
- const USAGE = 'usage: throughline runtime-errors <snapshot|diagnostics|ack|resolve|reopen|compact> [arguments] --json';
11
+ const USAGE = 'usage: throughline runtime-errors <enable|disable|snapshot|diagnostics|ack|resolve|reopen|compact> [arguments] --json';
11
12
 
12
13
  export function parseArgs(argv = []) {
13
14
  const command = argv[0];
14
- if (!['snapshot', 'diagnostics', 'ack', 'resolve', 'reopen', 'compact'].includes(command)) {
15
+ if (!['enable', 'disable', 'snapshot', 'diagnostics', 'ack', 'resolve', 'reopen', 'compact'].includes(command)) {
15
16
  throw new TypeError(USAGE);
16
17
  }
17
18
  const options = { command, json: false, afterCursor: 0, limit: 256, value: null };
@@ -47,7 +48,12 @@ export function run(argv = [], dependencies = {}) {
47
48
  try {
48
49
  const env = dependencies.env ?? process.env;
49
50
  let result;
50
- if (options.command === 'snapshot') {
51
+ if (options.command === 'enable' || options.command === 'disable') {
52
+ result = (dependencies.configure ?? setRuntimeErrorCollectionEnabled)(
53
+ options.command === 'enable',
54
+ { env },
55
+ );
56
+ } else if (options.command === 'snapshot') {
51
57
  result = (dependencies.readSnapshot ?? readRuntimeErrorSnapshot)({
52
58
  env,
53
59
  afterCursor: options.afterCursor,
@@ -1,16 +1,16 @@
1
1
  import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { spawnSync } from 'node:child_process';
4
- import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
4
+ import { mkdtempSync } from 'node:fs';
5
5
  import { tmpdir } from 'node:os';
6
- import { dirname, join } from 'node:path';
6
+ import { join } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
 
9
9
  import { parseArgs, run } from './runtime-errors.mjs';
10
- import { defaultFactoryReporterConfigPath } from '../runtime-error-store.mjs';
11
- import { applyWindowsPrivateAcl } from '../os/windows-acl-test-helper.mjs';
12
10
 
13
11
  test('runtime-errors CLI: strict command surface accepts no raw payload options', () => {
12
+ assert.equal(parseArgs(['enable', '--json']).command, 'enable');
13
+ assert.equal(parseArgs(['disable', '--json']).command, 'disable');
14
14
  assert.deepEqual(parseArgs(['snapshot', '--after-cursor', '2', '--limit', '3', '--json']), {
15
15
  command: 'snapshot', json: true, afterCursor: 2, limit: 3, value: null,
16
16
  });
@@ -52,16 +52,16 @@ test('runtime-errors CLI: snapshot and diagnostics are JSON-only and contain no
52
52
  XDG_CONFIG_HOME: join(root, 'config'),
53
53
  XDG_STATE_HOME: join(root, 'state'),
54
54
  };
55
- const configPath = defaultFactoryReporterConfigPath(env);
56
- mkdirSync(dirname(configPath), { recursive: true });
57
- writeFileSync(configPath, JSON.stringify({
58
- schema_version: '1.0',
59
- host: { id: 'test-host', profile: process.platform === 'win32' ? 'windows-native' : 'mac' },
60
- collection: { enabled: true },
61
- reporting: { enabled: false },
62
- }));
63
- applyWindowsPrivateAcl(configPath);
64
55
  const bin = new URL('../../bin/throughline.mjs', import.meta.url);
56
+ const enabled = spawnSync(process.execPath, [fileURLToPath(bin), 'runtime-errors', 'enable', '--json'], {
57
+ env,
58
+ encoding: 'utf8',
59
+ });
60
+ assert.equal(enabled.status, 0, enabled.stderr);
61
+ assert.deepEqual(JSON.parse(enabled.stdout), {
62
+ schema: 'throughline.runtime_error_config.v1',
63
+ collection: { enabled: true },
64
+ });
65
65
  for (const args of [
66
66
  ['runtime-errors', 'snapshot', '--json'],
67
67
  ['runtime-errors', 'diagnostics', '--json'],