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
@@ -0,0 +1,402 @@
1
+ import { createRequire } from 'node:module';
2
+ import { realpathSync } from 'node:fs';
3
+ import { dirname, isAbsolute, join, win32 } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ import { spawnPortableSync } from '../os/portable-spawn-sync.mjs';
7
+
8
+ const require = createRequire(import.meta.url);
9
+ const PACKAGE_VERSION = require('../../package.json').version;
10
+ const PACKAGE_ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
11
+ const DEFAULT_CLI_PATH = join(PACKAGE_ROOT, 'bin', 'throughline.mjs');
12
+ const POST_INSTALL_PHASE = 'post-install';
13
+ const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
14
+ const WINDOWS_NPM_OPTIONS = ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command'];
15
+
16
+ export const SELF_UPDATE_SCHEMA = 'throughline.self_update.v1';
17
+ export const SELF_UPDATE_IDENTITY_SCHEMA = 'throughline.self_update_identity.v1';
18
+
19
+ export function buildSelfUpdateIdentity({
20
+ version = PACKAGE_VERSION,
21
+ cliPath = DEFAULT_CLI_PATH,
22
+ } = {}) {
23
+ return {
24
+ schema: SELF_UPDATE_IDENTITY_SCHEMA,
25
+ version,
26
+ cliPath,
27
+ };
28
+ }
29
+
30
+ export function parseArgs(argv = []) {
31
+ if (argv.length === 0) return { json: false };
32
+ if (argv.length === 1 && argv[0] === '--json') return { json: true };
33
+ throw new TypeError('usage error');
34
+ }
35
+
36
+ function commandResult(runner, command, args, env, platform) {
37
+ return runner(command, args, {
38
+ encoding: 'utf8',
39
+ env,
40
+ input: '',
41
+ platform,
42
+ });
43
+ }
44
+
45
+ function windowsCommandResult(runner, command, args, env, platform) {
46
+ const quoted = [command, ...args]
47
+ .map((value) => `'${value.replaceAll("'", "''")}'`)
48
+ .join(' ');
49
+ const script = `& ${quoted}; ` +
50
+ 'if ($null -eq $LASTEXITCODE) { exit 1 }; exit $LASTEXITCODE';
51
+ return commandResult(
52
+ runner,
53
+ 'pwsh.exe',
54
+ [...WINDOWS_NPM_OPTIONS, script],
55
+ env,
56
+ platform,
57
+ );
58
+ }
59
+
60
+ function npmResult(runner, args, env, platform) {
61
+ if (platform === 'win32') return windowsCommandResult(runner, 'npm.cmd', args, env, platform);
62
+ return commandResult(runner, 'npm', args, env, platform);
63
+ }
64
+
65
+ function publicIdentityResult(runner, env, platform) {
66
+ const args = ['--self-update-identity'];
67
+ if (platform === 'win32') {
68
+ return windowsCommandResult(runner, 'throughline.cmd', args, env, platform);
69
+ }
70
+ return commandResult(runner, 'throughline', args, env, platform);
71
+ }
72
+
73
+ function succeeded(result) {
74
+ return result?.status === 0 && !result.error;
75
+ }
76
+
77
+ function parseRegistryVersion(result) {
78
+ if (!succeeded(result)) return null;
79
+ try {
80
+ const value = JSON.parse(result.stdout);
81
+ return typeof value === 'string' && VERSION_PATTERN.test(value) ? value : null;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ function parseInstalledVersion(result) {
88
+ if (!succeeded(result)) return null;
89
+ const value = result.stdout?.trim();
90
+ return typeof value === 'string' && VERSION_PATTERN.test(value) ? value : null;
91
+ }
92
+
93
+ export function resolveGlobalCliPath(result, platform = process.platform) {
94
+ if (!succeeded(result) || typeof result.stdout !== 'string') return null;
95
+ const lines = result.stdout.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
96
+ if (lines.length !== 1) return null;
97
+ const path = platform === 'win32' ? win32 : { isAbsolute, join };
98
+ if (!path.isAbsolute(lines[0])) return null;
99
+ return path.join(lines[0], 'throughline', 'bin', 'throughline.mjs');
100
+ }
101
+
102
+ export function validateSelfUpdateIdentity(value, {
103
+ version,
104
+ cliPath,
105
+ platform = process.platform,
106
+ canonicalizePath = realpathSync,
107
+ } = {}) {
108
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
109
+ const exact = ['cliPath', 'schema', 'version'];
110
+ if (Object.keys(value).sort().join('\0') !== exact.sort().join('\0')) return false;
111
+ if (value.schema !== SELF_UPDATE_IDENTITY_SCHEMA || value.version !== version ||
112
+ typeof value.cliPath !== 'string') return false;
113
+ const path = platform === 'win32' ? win32 : { isAbsolute };
114
+ if (!path.isAbsolute(value.cliPath) || !path.isAbsolute(cliPath ?? '')) return false;
115
+ try {
116
+ const actual = canonicalizePath(value.cliPath);
117
+ const expected = canonicalizePath(cliPath);
118
+ return platform === 'win32'
119
+ ? win32.normalize(actual).toLowerCase() === win32.normalize(expected).toLowerCase()
120
+ : actual === expected;
121
+ } catch {
122
+ return false;
123
+ }
124
+ }
125
+
126
+ export function validateMigrationResult(value) {
127
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
128
+ const exact = [
129
+ 'afterSchemaVersion',
130
+ 'beforeSchemaVersion',
131
+ 'schema',
132
+ 'status',
133
+ 'supportedSchemaVersion',
134
+ ];
135
+ if (Object.keys(value).sort().join('\0') !== exact.sort().join('\0')) return false;
136
+ if (value.schema !== 'throughline.database_migration.v1') return false;
137
+ if (!['migrated', 'already_current', 'not_applicable'].includes(value.status)) return false;
138
+ if (!Number.isInteger(value.supportedSchemaVersion) || value.supportedSchemaVersion < 1) return false;
139
+ if (value.status === 'not_applicable') {
140
+ return value.beforeSchemaVersion === null && value.afterSchemaVersion === null;
141
+ }
142
+ if (!Number.isInteger(value.beforeSchemaVersion) || !Number.isInteger(value.afterSchemaVersion)) {
143
+ return false;
144
+ }
145
+ if (value.afterSchemaVersion !== value.supportedSchemaVersion) return false;
146
+ if (value.status === 'already_current') return value.beforeSchemaVersion === value.afterSchemaVersion;
147
+ return value.beforeSchemaVersion < value.afterSchemaVersion;
148
+ }
149
+
150
+ export function validatePostUpdateDiagnostics(value, version) {
151
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
152
+ if (value.schema !== 'throughline.native_factory_diagnostics.v1' || value.version !== version) {
153
+ return false;
154
+ }
155
+ if (value.overall?.status !== 'ready') return false;
156
+ const database = value.databaseSchema;
157
+ if (!['ready', 'not_applicable'].includes(database?.status)) return false;
158
+ if (database.status === 'ready' &&
159
+ database.databaseSchemaVersion !== database.supportedDatabaseSchemaVersion) return false;
160
+ const hooks = value.hooks;
161
+ if (hooks?.status !== 'ready' ||
162
+ !['userPromptSubmit', 'postToolUse', 'stop'].every((event) => hooks.events?.[event] === 'ready')) {
163
+ return false;
164
+ }
165
+ return ['capture', 'restore', 'handoff'].every((check) =>
166
+ ['ready', 'not_applicable'].includes(value.readiness?.[check]?.status));
167
+ }
168
+
169
+ function parseJsonOutput(result, { requireSuccess = true } = {}) {
170
+ if ((requireSuccess && !succeeded(result)) || typeof result?.stdout !== 'string') return null;
171
+ try {
172
+ return JSON.parse(result.stdout);
173
+ } catch {
174
+ return null;
175
+ }
176
+ }
177
+
178
+ export function validatePostInstallResult(value, { beforeVersion, afterVersion } = {}) {
179
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
180
+ const exact = [
181
+ 'afterVersion',
182
+ 'beforeVersion',
183
+ 'diagnosticsStatus',
184
+ 'migrationStatus',
185
+ 'schema',
186
+ 'status',
187
+ ];
188
+ if (Object.keys(value).sort().join('\0') !== exact.sort().join('\0')) return false;
189
+ const expectedStatus = beforeVersion === afterVersion ? 'already_current' : 'updated';
190
+ return value.schema === SELF_UPDATE_SCHEMA &&
191
+ value.status === expectedStatus &&
192
+ value.beforeVersion === beforeVersion &&
193
+ value.afterVersion === afterVersion &&
194
+ ['migrated', 'already_current', 'not_applicable'].includes(value.migrationStatus) &&
195
+ value.diagnosticsStatus === 'ready';
196
+ }
197
+
198
+ function validatePostInstallFailure(value, version) {
199
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
200
+ const exact = ['schema', 'stage', 'status', 'version'];
201
+ return Object.keys(value).sort().join('\0') === exact.sort().join('\0') &&
202
+ value.schema === SELF_UPDATE_SCHEMA &&
203
+ value.status === 'failed' &&
204
+ typeof value.stage === 'string' && value.stage.length > 0 &&
205
+ value.version === version;
206
+ }
207
+
208
+ function writeChildStderr(stderr, result) {
209
+ if (typeof result?.stderr === 'string' && result.stderr.length > 0) stderr.write(result.stderr);
210
+ }
211
+
212
+ function emitFailure({ json, stdout, stderr, stage, version = PACKAGE_VERSION }, exitCode = 1) {
213
+ if (json) {
214
+ stdout.write(`${JSON.stringify({
215
+ schema: SELF_UPDATE_SCHEMA,
216
+ status: 'failed',
217
+ stage,
218
+ version,
219
+ })}\n`);
220
+ } else {
221
+ stderr.write(`[throughline self-update] failed: ${stage}\n`);
222
+ }
223
+ return exitCode;
224
+ }
225
+
226
+ function emitCommandFailure(options, result, exitCode = 1) {
227
+ writeChildStderr(options.stderr, result);
228
+ return emitFailure(options, exitCode);
229
+ }
230
+
231
+ function emitSuccess({ json, stdout }, result) {
232
+ if (json) stdout.write(`${JSON.stringify(result)}\n`);
233
+ else {
234
+ stdout.write(
235
+ `Throughline ${result.beforeVersion} -> ${result.afterVersion}: ${result.status}; ` +
236
+ `migration=${result.migrationStatus}; diagnostics=${result.diagnosticsStatus}\n`,
237
+ );
238
+ }
239
+ return 0;
240
+ }
241
+
242
+ function finishUpdate({
243
+ json,
244
+ stdout,
245
+ stderr,
246
+ env,
247
+ runner,
248
+ packageVersion,
249
+ nodePath,
250
+ cliPath,
251
+ platform,
252
+ }) {
253
+ const beforeVersion = env.THROUGHLINE_SELF_UPDATE_BEFORE_VERSION;
254
+ if (!VERSION_PATTERN.test(beforeVersion ?? '')) {
255
+ return emitFailure({ json, stdout, stderr, stage: 'invalid_update_phase', version: packageVersion });
256
+ }
257
+ if (env.THROUGHLINE_SELF_UPDATE_EXPECTED_VERSION !== packageVersion) {
258
+ return emitFailure({ json, stdout, stderr, stage: 'installed_version_mismatch', version: packageVersion });
259
+ }
260
+
261
+ const registry = npmResult(runner, ['view', 'throughline', 'version', '--json'], env, platform);
262
+ const latestVersion = parseRegistryVersion(registry);
263
+ if (latestVersion === null || latestVersion !== packageVersion) {
264
+ return emitCommandFailure(
265
+ { json, stdout, stderr, stage: 'version_verification_failed', version: packageVersion },
266
+ registry,
267
+ );
268
+ }
269
+
270
+ const install = commandResult(runner, nodePath, [cliPath, 'install'], env, platform);
271
+ if (!succeeded(install)) {
272
+ return emitCommandFailure(
273
+ { json, stdout, stderr, stage: 'integration_install_failed', version: packageVersion },
274
+ install,
275
+ );
276
+ }
277
+
278
+ const migrationResult = commandResult(
279
+ runner, nodePath, [cliPath, 'migrate', '--json'], env, platform,
280
+ );
281
+ const migration = parseJsonOutput(migrationResult);
282
+ if (!validateMigrationResult(migration)) {
283
+ return emitCommandFailure(
284
+ { json, stdout, stderr, stage: 'database_migration_failed', version: packageVersion },
285
+ migrationResult,
286
+ );
287
+ }
288
+
289
+ const diagnosticsResult = commandResult(
290
+ runner, nodePath, [cliPath, 'factory-diagnostics', '--json'], env, platform,
291
+ );
292
+ const diagnostics = parseJsonOutput(diagnosticsResult);
293
+ if (!validatePostUpdateDiagnostics(diagnostics, packageVersion)) {
294
+ return emitCommandFailure(
295
+ { json, stdout, stderr, stage: 'post_update_diagnostics_failed', version: packageVersion },
296
+ diagnosticsResult,
297
+ );
298
+ }
299
+
300
+ const result = {
301
+ schema: SELF_UPDATE_SCHEMA,
302
+ status: beforeVersion === packageVersion ? 'already_current' : 'updated',
303
+ beforeVersion,
304
+ afterVersion: packageVersion,
305
+ migrationStatus: migration.status,
306
+ diagnosticsStatus: 'ready',
307
+ };
308
+ return emitSuccess({ json, stdout }, result);
309
+ }
310
+
311
+ export function run(argv = [], {
312
+ stdout = process.stdout,
313
+ stderr = process.stderr,
314
+ env = process.env,
315
+ runner = spawnPortableSync,
316
+ packageVersion = PACKAGE_VERSION,
317
+ nodePath = process.execPath,
318
+ cliPath = DEFAULT_CLI_PATH,
319
+ platform = process.platform,
320
+ canonicalizePath = realpathSync,
321
+ } = {}) {
322
+ let args;
323
+ try {
324
+ args = parseArgs(argv);
325
+ } catch {
326
+ return emitFailure({
327
+ json: argv.includes('--json'), stdout, stderr, stage: 'invalid_request', version: packageVersion,
328
+ }, 2);
329
+ }
330
+
331
+ if (env.THROUGHLINE_SELF_UPDATE_PHASE === POST_INSTALL_PHASE) {
332
+ return finishUpdate({
333
+ ...args, stdout, stderr, env, runner, packageVersion, nodePath, cliPath, platform,
334
+ });
335
+ }
336
+
337
+ const update = npmResult(runner, ['install', '-g', 'throughline@latest'], env, platform);
338
+ if (!succeeded(update)) {
339
+ return emitFailure({ ...args, stdout, stderr, stage: 'package_update_failed', version: packageVersion });
340
+ }
341
+
342
+ const globalRootResult = npmResult(runner, ['root', '--global'], env, platform);
343
+ const installedCliPath = resolveGlobalCliPath(globalRootResult, platform);
344
+ if (installedCliPath === null) {
345
+ return emitCommandFailure(
346
+ { ...args, stdout, stderr, stage: 'global_cli_resolution_failed', version: packageVersion },
347
+ globalRootResult,
348
+ );
349
+ }
350
+
351
+ const installedVersionResult = commandResult(
352
+ runner, nodePath, [installedCliPath, '--version'], env, platform,
353
+ );
354
+ const installedVersion = parseInstalledVersion(installedVersionResult);
355
+ if (installedVersion === null) {
356
+ return emitCommandFailure(
357
+ { ...args, stdout, stderr, stage: 'installed_version_verification_failed', version: packageVersion },
358
+ installedVersionResult,
359
+ );
360
+ }
361
+
362
+ const identityResult = publicIdentityResult(runner, env, platform);
363
+ const identity = parseJsonOutput(identityResult);
364
+ if (!validateSelfUpdateIdentity(identity, {
365
+ version: installedVersion,
366
+ cliPath: installedCliPath,
367
+ platform,
368
+ canonicalizePath,
369
+ })) {
370
+ return emitCommandFailure(
371
+ { ...args, stdout, stderr, stage: 'public_cli_mismatch', version: installedVersion },
372
+ identityResult,
373
+ );
374
+ }
375
+
376
+ const child = commandResult(runner, nodePath, [installedCliPath, 'self-update', '--json'], {
377
+ ...env,
378
+ THROUGHLINE_SELF_UPDATE_PHASE: POST_INSTALL_PHASE,
379
+ THROUGHLINE_SELF_UPDATE_BEFORE_VERSION: packageVersion,
380
+ THROUGHLINE_SELF_UPDATE_EXPECTED_VERSION: installedVersion,
381
+ }, platform);
382
+ writeChildStderr(stderr, child);
383
+ const childPayload = parseJsonOutput(child, { requireSuccess: false });
384
+ if (succeeded(child) && validatePostInstallResult(childPayload, {
385
+ beforeVersion: packageVersion,
386
+ afterVersion: installedVersion,
387
+ })) {
388
+ return emitSuccess(args.json ? { json: true, stdout } : { json: false, stdout }, childPayload);
389
+ }
390
+ if (!succeeded(child) && validatePostInstallFailure(childPayload, installedVersion)) {
391
+ if (args.json) stdout.write(`${JSON.stringify(childPayload)}\n`);
392
+ else stderr.write(`[throughline self-update] failed: ${childPayload.stage}\n`);
393
+ return Number.isInteger(child.status) && child.status > 0 ? child.status : 1;
394
+ }
395
+ return emitFailure({
396
+ ...args,
397
+ stdout,
398
+ stderr,
399
+ stage: 'post_install_protocol_failed',
400
+ version: installedVersion,
401
+ });
402
+ }