throughline 0.10.0 → 0.10.1
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/CHANGELOG.md +24 -1
- package/package.json +1 -1
- package/src/auditor-context.mjs +2 -1
- package/src/cli/codex-handoff-start.mjs +5 -23
- package/src/cli/codex-summarize.mjs +2 -1
- package/src/cli/codex-visibility-smoke.mjs +5 -4
- package/src/cli/grok-continue.mjs +8 -21
- package/src/cli/runtime-errors.test.mjs +1 -1
- package/src/codex-capture.mjs +14 -18
- package/src/codex-handoff-model-smoke.mjs +1 -1
- package/src/codex-sidecar.mjs +1 -1
- package/src/completed-turn-receipts.mjs +2 -29
- package/src/completed-turn-receipts.test.mjs +1 -1
- package/src/haiku-summarizer.mjs +1 -1
- package/src/handoff-record.mjs +3 -1
- package/src/hosts/claude.mjs +27 -0
- package/src/hosts/codex.mjs +28 -0
- package/src/hosts/grok.mjs +96 -0
- package/src/{hook-envelope.test.mjs → hosts/grok.test.mjs} +1 -1
- package/src/hosts/identity.mjs +71 -0
- package/src/hosts/identity.test.mjs +65 -0
- package/src/hosts/index.mjs +37 -0
- package/src/os/macos-terminal.mjs +44 -0
- package/src/os/open-url.mjs +16 -0
- package/src/os/paths.mjs +12 -0
- package/src/os/shell.mjs +16 -0
- package/src/os/windows-acl.mjs +57 -0
- package/src/project-path.mjs +4 -4
- package/src/prompt-submit.mjs +23 -55
- package/src/prompt-submit.test.mjs +27 -10
- package/src/runtime-error-hook.test.mjs +1 -1
- package/src/runtime-error-store.mjs +3 -40
- package/src/runtime-error-store.test.mjs +1 -1
- package/src/session-start.mjs +12 -4
- package/src/state-file.mjs +6 -5
- package/src/token-monitor.mjs +7 -6
- package/src/turn-processor.mjs +2 -2
- package/src/hook-envelope.mjs +0 -51
- /package/src/{portable-spawn-sync.mjs → os/portable-spawn-sync.mjs} +0 -0
- /package/src/{portable-spawn-sync.test.mjs → os/portable-spawn-sync.test.mjs} +0 -0
- /package/src/{windows-acl-test-helper.mjs → os/windows-acl-test-helper.mjs} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,25 @@ shipped to npm but were not individually tagged on GitHub.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.10.1] — 2026-08-23
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Internal refactor with no behavior change. Vendor (hook host) identity and
|
|
18
|
+
per-host behavior moved into `src/hosts/` — `hosts/identity.mjs` is the
|
|
19
|
+
single source of the `codex:` / `grok:` session prefixes (previously
|
|
20
|
+
duplicated across four files), and the shared hook entrypoints
|
|
21
|
+
(`session-start` / `prompt-submit` / `process-turn`) now branch through
|
|
22
|
+
`hosts/{claude,codex,grok}.mjs` adapters instead of inline host checks.
|
|
23
|
+
`src/hook-envelope.mjs` is merged into `hosts/grok.mjs`.
|
|
24
|
+
- OS-specific code moved into `src/os/` — the Windows owner-only ACL
|
|
25
|
+
PowerShell implementation that was duplicated verbatim in
|
|
26
|
+
`runtime-error-store` and `completed-turn-receipts` is now the single
|
|
27
|
+
`os/windows-acl.mjs`, alongside macOS Terminal launch, OS URL open,
|
|
28
|
+
shell/AppleScript quoting, Windows path case folding, and the portable
|
|
29
|
+
spawn helper. DB schema, injection contract, and every CLI surface are
|
|
30
|
+
unchanged; full regression is 761 pass / 0 fail.
|
|
31
|
+
|
|
13
32
|
## [0.10.0] — 2026-08-17
|
|
14
33
|
|
|
15
34
|
### Added
|
|
@@ -1271,7 +1290,11 @@ two attempts, instrument first instead of patching again.
|
|
|
1271
1290
|
|
|
1272
1291
|
---
|
|
1273
1292
|
|
|
1274
|
-
[Unreleased]: https://github.com/kitepon-rgb/Throughline/compare/v0.
|
|
1293
|
+
[Unreleased]: https://github.com/kitepon-rgb/Throughline/compare/v0.10.1...HEAD
|
|
1294
|
+
[0.10.1]: https://github.com/kitepon-rgb/Throughline/compare/v0.10.0...v0.10.1
|
|
1295
|
+
[0.10.0]: https://github.com/kitepon-rgb/Throughline/compare/v0.9.1...v0.10.0
|
|
1296
|
+
[0.9.1]: https://github.com/kitepon-rgb/Throughline/compare/v0.9.0...v0.9.1
|
|
1297
|
+
[0.9.0]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.9...v0.9.0
|
|
1275
1298
|
[0.8.9]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.8...v0.8.9
|
|
1276
1299
|
[0.8.8]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.7...v0.8.8
|
|
1277
1300
|
[0.8.7]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.6...v0.8.7
|
package/package.json
CHANGED
package/src/auditor-context.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { homedir } from 'node:os';
|
|
|
3
3
|
import { isAbsolute, join, resolve, sep } from 'node:path';
|
|
4
4
|
import { DatabaseSync } from 'node:sqlite';
|
|
5
5
|
import { buildBodyRowsFromActiveTurns } from './codex-capture.mjs';
|
|
6
|
+
import { isCodexSessionId } from './hosts/identity.mjs';
|
|
6
7
|
import { parseCodexRolloutFile } from './codex-rollout-memory.mjs';
|
|
7
8
|
import { getLogicalTurnGroups } from './transcript-reader.mjs';
|
|
8
9
|
import { hashAuditorBody, normalizeAuditorBody } from './body-digest.mjs';
|
|
@@ -119,7 +120,7 @@ export function readAuditorContext({
|
|
|
119
120
|
|
|
120
121
|
const latest = completed.at(-1);
|
|
121
122
|
const stableTurnIdentityAvailable = Number.isInteger(expectedTurnNumber) && expectedTurnNumber >= 0;
|
|
122
|
-
const codexPairIdentity = sessionId
|
|
123
|
+
const codexPairIdentity = isCodexSessionId(sessionId) && expectedOriginSessionId === sessionId;
|
|
123
124
|
const expectationComplete =
|
|
124
125
|
typeof expectedOriginSessionId === 'string' && expectedOriginSessionId.length > 0 &&
|
|
125
126
|
(stableTurnIdentityAvailable || codexPairIdentity) &&
|
|
@@ -6,7 +6,9 @@ import { buildCodexHandoffModelSmokePrompt } from '../codex-handoff-model-smoke.
|
|
|
6
6
|
import { runCodexNewThreadHandoff } from '../codex-app-server.mjs';
|
|
7
7
|
import { estimateTokens } from '../token-estimator.mjs';
|
|
8
8
|
import { sameProjectPath } from '../project-path.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import { shQuote } from '../os/shell.mjs';
|
|
10
|
+
import { openUrlWithOsHandler } from '../os/open-url.mjs';
|
|
11
|
+
import { runTerminalDoScript } from '../os/macos-terminal.mjs';
|
|
10
12
|
|
|
11
13
|
async function readStdin() {
|
|
12
14
|
let raw = '';
|
|
@@ -319,12 +321,7 @@ function openStartedCodexThread({ threadId, host, cwd }) {
|
|
|
319
321
|
|
|
320
322
|
if (resolvedHost === 'desktop' || resolvedHost === 'vscode') {
|
|
321
323
|
const url = resolvedHost === 'desktop' ? desktopUrl : vscodeUrl;
|
|
322
|
-
const result =
|
|
323
|
-
process.platform === 'darwin'
|
|
324
|
-
? spawnSync('open', [url], { encoding: 'utf8' })
|
|
325
|
-
: process.platform === 'win32'
|
|
326
|
-
? spawnSync('cmd.exe', ['/c', 'start', '', url], { encoding: 'utf8' })
|
|
327
|
-
: spawnSync('xdg-open', [url], { encoding: 'utf8' });
|
|
324
|
+
const result = openUrlWithOsHandler(url);
|
|
328
325
|
if (result.status !== 0) {
|
|
329
326
|
return {
|
|
330
327
|
status: 'failed',
|
|
@@ -353,14 +350,7 @@ function openStartedCodexThread({ threadId, host, cwd }) {
|
|
|
353
350
|
if (resolvedHost === 'cli') {
|
|
354
351
|
if (process.platform === 'darwin') {
|
|
355
352
|
const shellCommand = `cd ${shQuote(cwd)} && TERM=xterm-256color codex resume ${shQuote(threadId)} --no-alt-screen`;
|
|
356
|
-
const result =
|
|
357
|
-
input: `tell application "Terminal"
|
|
358
|
-
activate
|
|
359
|
-
do script ${appleString(shellCommand)}
|
|
360
|
-
end tell
|
|
361
|
-
`,
|
|
362
|
-
encoding: 'utf8',
|
|
363
|
-
});
|
|
353
|
+
const result = runTerminalDoScript(shellCommand);
|
|
364
354
|
if (result.status !== 0) {
|
|
365
355
|
return {
|
|
366
356
|
status: 'failed',
|
|
@@ -409,14 +399,6 @@ end tell
|
|
|
409
399
|
};
|
|
410
400
|
}
|
|
411
401
|
|
|
412
|
-
function shQuote(value) {
|
|
413
|
-
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function appleString(value) {
|
|
417
|
-
return `"${String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
402
|
export async function run(args) {
|
|
421
403
|
let parsed;
|
|
422
404
|
try {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getDb } from '../db.mjs';
|
|
2
|
+
import { isCodexSessionId } from '../hosts/identity.mjs';
|
|
2
3
|
import {
|
|
3
4
|
L2_WINDOW,
|
|
4
5
|
countDistinctBodyTurns,
|
|
@@ -94,7 +95,7 @@ function insertSkeleton(db, { sessionId, originSessionId, turnNumber, summary, c
|
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
export function summarizeCodexSession(db, { sessionId, projectPath, max, env = process.env }) {
|
|
97
|
-
if (!sessionId
|
|
98
|
+
if (!isCodexSessionId(sessionId)) {
|
|
98
99
|
throw new Error('codex-summarize requires a codex:<thread-id> session');
|
|
99
100
|
}
|
|
100
101
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { CODEX_SESSION_PREFIX, codexSessionIdToThreadId, isCodexSessionId } from '../hosts/identity.mjs';
|
|
2
3
|
|
|
3
4
|
import { getDb } from '../db.mjs';
|
|
4
5
|
import { buildHandoffRecord } from '../handoff-record.mjs';
|
|
@@ -82,10 +83,10 @@ function parseArgs(args) {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
if (!out.sessionId && out.codexThreadId) {
|
|
85
|
-
out.sessionId =
|
|
86
|
+
out.sessionId = `${CODEX_SESSION_PREFIX}${out.codexThreadId}`;
|
|
86
87
|
}
|
|
87
|
-
if (!out.codexThreadId && out.sessionId
|
|
88
|
-
out.codexThreadId = out.sessionId
|
|
88
|
+
if (!out.codexThreadId && isCodexSessionId(out.sessionId)) {
|
|
89
|
+
out.codexThreadId = codexSessionIdToThreadId(out.sessionId);
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
return out;
|
|
@@ -154,7 +155,7 @@ export async function run(args) {
|
|
|
154
155
|
const db = getDb();
|
|
155
156
|
const sessionId = parsed.sessionId ?? findLatestCodexSessionId(db, process.cwd());
|
|
156
157
|
const codexThreadId =
|
|
157
|
-
parsed.codexThreadId ?? (sessionId
|
|
158
|
+
parsed.codexThreadId ?? codexSessionIdToThreadId(sessionId);
|
|
158
159
|
if (!sessionId || !codexThreadId) {
|
|
159
160
|
process.stderr.write(
|
|
160
161
|
'[codex-visibility-smoke] pass --session codex:<thread-id> or --codex-thread-id <id>.\n',
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
1
|
import { randomUUID } from 'node:crypto';
|
|
3
2
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
3
|
import { homedir, tmpdir } from 'node:os';
|
|
5
4
|
import { delimiter, join } from 'node:path';
|
|
6
5
|
|
|
7
6
|
import { readHandoffContext, readSessionProjectPath } from './handoff-context.mjs';
|
|
7
|
+
import { shQuote } from '../os/shell.mjs';
|
|
8
|
+
import { appleScriptForTerminalExec, runTerminalScriptDetached } from '../os/macos-terminal.mjs';
|
|
8
9
|
|
|
9
10
|
export const GROK_CONTINUE_PREAMBLE = 'この発言は直前 Throughline 席の履歴を前提とする。';
|
|
10
11
|
export const GROK_CONTINUE_REQUEST = '直前の作業の自然な続きとして応答すること。';
|
|
@@ -29,10 +30,6 @@ export function buildGrokContinuePrompt(context) {
|
|
|
29
30
|
return `${GROK_CONTINUE_PREAMBLE}\n\n${context}\n\n${GROK_CONTINUE_REQUEST}\n\n${GROK_CONTINUE_WAIT}`;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
export function shQuote(value) {
|
|
33
|
-
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
33
|
export function resolveGrokBin({
|
|
37
34
|
home = homedir(),
|
|
38
35
|
env = process.env,
|
|
@@ -62,15 +59,8 @@ export function buildLaunchScript({ cwd, grokBin, sessionUuid, promptFile }) {
|
|
|
62
59
|
].join('\n');
|
|
63
60
|
}
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
'tell application "Terminal"',
|
|
68
|
-
' activate',
|
|
69
|
-
` do script "exec " & quoted form of ${JSON.stringify(launchScriptPath)}`,
|
|
70
|
-
'end tell',
|
|
71
|
-
'',
|
|
72
|
-
].join('\n');
|
|
73
|
-
}
|
|
62
|
+
// macOS Terminal 起動の実体は os/macos-terminal.mjs (OS 境界)。
|
|
63
|
+
export const appleScriptForLaunch = appleScriptForTerminalExec;
|
|
74
64
|
|
|
75
65
|
export function buildContinuePlan({
|
|
76
66
|
context,
|
|
@@ -113,13 +103,10 @@ export function writeLaunchArtifacts({
|
|
|
113
103
|
return { promptFile, launchFile };
|
|
114
104
|
}
|
|
115
105
|
|
|
116
|
-
export function defaultSpawnLaunch({ launchFile, spawnImpl
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
});
|
|
121
|
-
child.unref?.();
|
|
122
|
-
return child;
|
|
106
|
+
export function defaultSpawnLaunch({ launchFile, spawnImpl }) {
|
|
107
|
+
return spawnImpl === undefined
|
|
108
|
+
? runTerminalScriptDetached({ launchFile })
|
|
109
|
+
: runTerminalScriptDetached({ launchFile, spawnImpl });
|
|
123
110
|
}
|
|
124
111
|
|
|
125
112
|
export function run(argv = [], {
|
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
|
|
9
9
|
import { parseArgs, run } from './runtime-errors.mjs';
|
|
10
10
|
import { defaultFactoryReporterConfigPath } from '../runtime-error-store.mjs';
|
|
11
|
-
import { applyWindowsPrivateAcl } from '../windows-acl-test-helper.mjs';
|
|
11
|
+
import { applyWindowsPrivateAcl } from '../os/windows-acl-test-helper.mjs';
|
|
12
12
|
|
|
13
13
|
test('runtime-errors CLI: strict command surface accepts no raw payload options', () => {
|
|
14
14
|
assert.deepEqual(parseArgs(['snapshot', '--after-cursor', '2', '--limit', '3', '--json']), {
|
package/src/codex-capture.mjs
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
import { parseCodexRolloutFile } from './codex-rollout-memory.mjs';
|
|
2
2
|
import { defaultCodexHome, findCodexThreadCandidate } from './codex-thread-index.mjs';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function codexSessionIdToThreadId(sessionId) {
|
|
18
|
-
if (!isCodexThroughlineSessionId(sessionId)) return null;
|
|
19
|
-
return sessionId.slice(CODEX_SESSION_PREFIX.length) || null;
|
|
20
|
-
}
|
|
3
|
+
import {
|
|
4
|
+
CODEX_SESSION_PREFIX,
|
|
5
|
+
buildCodexThroughlineSessionId,
|
|
6
|
+
codexSessionIdToThreadId,
|
|
7
|
+
isCodexSessionId,
|
|
8
|
+
} from './hosts/identity.mjs';
|
|
9
|
+
|
|
10
|
+
// session identity の正本は hosts/identity.mjs。既存 import 面のため再 export する。
|
|
11
|
+
export {
|
|
12
|
+
CODEX_SESSION_PREFIX,
|
|
13
|
+
buildCodexThroughlineSessionId,
|
|
14
|
+
codexSessionIdToThreadId,
|
|
15
|
+
};
|
|
16
|
+
export const isCodexThroughlineSessionId = isCodexSessionId;
|
|
21
17
|
|
|
22
18
|
export function captureCodexRolloutToDb(
|
|
23
19
|
db,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawnPortableSync } from './portable-spawn-sync.mjs';
|
|
1
|
+
import { spawnPortableSync } from './os/portable-spawn-sync.mjs';
|
|
2
2
|
|
|
3
3
|
export const CODEX_HANDOFF_MODEL_SMOKE_ENV = 'THROUGHLINE_EXPERIMENTAL_CODEX_HANDOFF_MODEL_SMOKE';
|
|
4
4
|
export const DEFAULT_CODEX_HANDOFF_MODEL_SMOKE_TIMEOUT_MS = 120_000;
|
package/src/codex-sidecar.mjs
CHANGED
|
@@ -8,12 +8,12 @@ import {
|
|
|
8
8
|
rmSync,
|
|
9
9
|
writeFileSync,
|
|
10
10
|
} from 'node:fs';
|
|
11
|
-
import
|
|
12
|
-
import { homedir, platform as hostPlatform } from 'node:os';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
13
12
|
import { dirname, join } from 'node:path';
|
|
14
13
|
import { DatabaseSync } from 'node:sqlite';
|
|
15
14
|
import { hashAuditorBody } from './body-digest.mjs';
|
|
16
15
|
import { normalizeProjectPathForCompare } from './project-path.mjs';
|
|
16
|
+
import { applyAndVerifyWindowsAcl, isWindows, verifyWindowsAcl } from './os/windows-acl.mjs';
|
|
17
17
|
|
|
18
18
|
export const COMPLETED_TURN_RECEIPT_STORE_SCHEMA = 'throughline.completed_turn_receipts.v1';
|
|
19
19
|
export const COMPLETED_TURN_RECEIPT_SCHEMA_VERSION = '1.0';
|
|
@@ -21,7 +21,6 @@ export const COMPLETED_TURN_RECEIPT_LIMIT = 256;
|
|
|
21
21
|
|
|
22
22
|
const PRIVATE_DIRECTORY_CAPABILITY = Symbol('throughline.completed-turn-receipt-directory');
|
|
23
23
|
// CI実測でPowerShellコールドスタートが3.0〜3.2秒に達しflakeしたため15秒 (run 29586852389 / 29628634501)
|
|
24
|
-
const WINDOWS_ACL_TIMEOUT_MS = 15_000;
|
|
25
24
|
|
|
26
25
|
export function defaultCompletedTurnReceiptStorePath(projectSha256, env = process.env) {
|
|
27
26
|
if (!isSha256(projectSha256)) throw new TypeError('projectSha256 must be a SHA-256 digest');
|
|
@@ -351,30 +350,4 @@ function assertExactOptions(options) {
|
|
|
351
350
|
throw new TypeError('completed turn receipt options are invalid');
|
|
352
351
|
}
|
|
353
352
|
}
|
|
354
|
-
function isWindows(env = process.env) { return env.OS === 'Windows_NT' || hostPlatform() === 'win32'; }
|
|
355
|
-
|
|
356
|
-
function applyAndVerifyWindowsAcl(path, directory) { runWindowsAclScript(path, directory, WINDOWS_ACL_APPLY_SCRIPT); }
|
|
357
|
-
function verifyWindowsAcl(path, directory) { runWindowsAclScript(path, directory, WINDOWS_ACL_VERIFY_SCRIPT); }
|
|
358
|
-
function runWindowsAclScript(path, directory, script) {
|
|
359
|
-
const result = childProcess.spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
360
|
-
env: { ...process.env, THROUGHLINE_ACL_PATH: path, THROUGHLINE_ACL_DIRECTORY: directory ? '1' : '0' },
|
|
361
|
-
stdio: 'ignore', timeout: WINDOWS_ACL_TIMEOUT_MS, windowsHide: true,
|
|
362
|
-
});
|
|
363
|
-
if (result.status !== 0) throw new Error('Windows owner-only ACL verification failed');
|
|
364
|
-
}
|
|
365
353
|
|
|
366
|
-
const WINDOWS_ACL_VERIFY_SCRIPT = String.raw`
|
|
367
|
-
$p=$env:THROUGHLINE_ACL_PATH; $isDir=$env:THROUGHLINE_ACL_DIRECTORY -eq '1'; $sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
368
|
-
$acl=if($isDir){[System.IO.Directory]::GetAccessControl($p)}else{[System.IO.File]::GetAccessControl($p)}
|
|
369
|
-
$owner=$acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value
|
|
370
|
-
if($owner -ne $sid){exit 41}; $rules=@($acl.GetAccessRules($true,$true,[System.Security.Principal.SecurityIdentifier])); if($rules.Count -ne 1){exit 42}
|
|
371
|
-
$r=$rules[0]; if($r.IdentityReference.Value -ne $sid -or $r.AccessControlType -ne 'Allow' -or $r.IsInherited -or ($r.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl) -ne [System.Security.AccessControl.FileSystemRights]::FullControl){exit 43}
|
|
372
|
-
`;
|
|
373
|
-
|
|
374
|
-
const WINDOWS_ACL_APPLY_SCRIPT = String.raw`
|
|
375
|
-
$p=$env:THROUGHLINE_ACL_PATH; $sid=[System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
376
|
-
$isDir=$env:THROUGHLINE_ACL_DIRECTORY -eq '1'; $acl=if($isDir){New-Object System.Security.AccessControl.DirectorySecurity}else{New-Object System.Security.AccessControl.FileSecurity}; $acl.SetAccessRuleProtection($true,$false)
|
|
377
|
-
$flags=if($isDir){[System.Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit'}else{[System.Security.AccessControl.InheritanceFlags]::None}
|
|
378
|
-
$rule=New-Object System.Security.AccessControl.FileSystemAccessRule($sid,'FullControl',$flags,[System.Security.AccessControl.PropagationFlags]::None,[System.Security.AccessControl.AccessControlType]::Allow)
|
|
379
|
-
$acl.SetOwner($sid); $acl.AddAccessRule($rule); if($isDir){[System.IO.Directory]::SetAccessControl($p,$acl)}else{[System.IO.File]::SetAccessControl($p,$acl)}
|
|
380
|
-
` + WINDOWS_ACL_VERIFY_SCRIPT;
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
writeCompletedTurnReceipt,
|
|
13
13
|
} from './completed-turn-receipts.mjs';
|
|
14
14
|
import { seedCompletedTurnReceiptStore } from './completed-turn-receipts-test-fixture.mjs';
|
|
15
|
-
import { verifyWindowsPrivateAcl } from './windows-acl-test-helper.mjs';
|
|
15
|
+
import { verifyWindowsPrivateAcl } from './os/windows-acl-test-helper.mjs';
|
|
16
16
|
|
|
17
17
|
function sandbox() {
|
|
18
18
|
const root = mkdtempSync(join(tmpdir(), 'throughline-completed-receipts-'));
|
package/src/haiku-summarizer.mjs
CHANGED
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
CODEX_SIDECAR_STATUS,
|
|
51
51
|
runCodexSidecarCommand,
|
|
52
52
|
} from './codex-sidecar.mjs';
|
|
53
|
-
import { spawnPortableSync } from './portable-spawn-sync.mjs';
|
|
53
|
+
import { spawnPortableSync } from './os/portable-spawn-sync.mjs';
|
|
54
54
|
|
|
55
55
|
const MODEL = 'claude-haiku-4-5-20251001';
|
|
56
56
|
// Codex CLI 要約の既定。gpt-5.6-luna@low@1/5 は 2026-07-17 の実測評価で選定
|
package/src/handoff-record.mjs
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
* persisted to DB; adapters can render it for Claude, Codex, or diagnostics.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { CODEX_SESSION_PREFIX } from './hosts/identity.mjs';
|
|
10
|
+
|
|
9
11
|
export const HANDOFF_RECORD_VERSION = 1;
|
|
10
12
|
export const N_RECENT_L2 = 20;
|
|
11
|
-
export
|
|
13
|
+
export { CODEX_SESSION_PREFIX };
|
|
12
14
|
|
|
13
15
|
const DEFAULT_INTENT = 'continue implementation';
|
|
14
16
|
const DEFAULT_CONSTRAINTS = [
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hosts/claude.mjs — Claude host adapter
|
|
3
|
+
*
|
|
4
|
+
* Claude は Throughline の基準 host。hook payload は snake_case のまま届き、
|
|
5
|
+
* 引き継ぎ注入は UserPromptSubmit hook の stdout でモデルへ渡る。
|
|
6
|
+
*/
|
|
7
|
+
import { CLAUDE_HOST, hostOfSessionId } from './identity.mjs';
|
|
8
|
+
|
|
9
|
+
export const claudeHostAdapter = Object.freeze({
|
|
10
|
+
host: CLAUDE_HOST,
|
|
11
|
+
matchesSessionId: (sessionId) => hostOfSessionId(sessionId) === CLAUDE_HOST,
|
|
12
|
+
// Claude Stop payload の last_assistant_message を transcript 可視化の
|
|
13
|
+
// barrier に使う (ADR 0012)。
|
|
14
|
+
waitsForStopTranscriptFlush: true,
|
|
15
|
+
// Claude は UserPromptSubmit stdout がそのままモデル可視 context になる。
|
|
16
|
+
deliverHandoffInjection({ text, stdout = process.stdout }) {
|
|
17
|
+
stdout.write(text + '\n');
|
|
18
|
+
return { delivered: true };
|
|
19
|
+
},
|
|
20
|
+
// Claude の prompt は裸の slash command がそのまま届く。
|
|
21
|
+
resolveCommandPrompt({ prompt }) {
|
|
22
|
+
return prompt;
|
|
23
|
+
},
|
|
24
|
+
afterBatonWrite() {
|
|
25
|
+
return { launched: false };
|
|
26
|
+
},
|
|
27
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hosts/codex.mjs — Codex host adapter
|
|
3
|
+
*
|
|
4
|
+
* Codex session (`codex:<thread_id>`) は Claude-facing hook (SessionStart /
|
|
5
|
+
* UserPromptSubmit / Stop) を通らず、専用入口 `throughline codex-hook`
|
|
6
|
+
* ([src/cli/codex-hook.mjs](../cli/codex-hook.mjs)) で capture される。
|
|
7
|
+
* この adapter は共有コードが Codex session を Claude と取り違えないための
|
|
8
|
+
* 識別と、Claude-facing hook に Codex session が流入した場合の既存挙動
|
|
9
|
+
* (Claude と同じ経路で処理する) を明文化する。
|
|
10
|
+
*/
|
|
11
|
+
import { CODEX_HOST, isCodexSessionId } from './identity.mjs';
|
|
12
|
+
|
|
13
|
+
export const codexHostAdapter = Object.freeze({
|
|
14
|
+
host: CODEX_HOST,
|
|
15
|
+
matchesSessionId: isCodexSessionId,
|
|
16
|
+
// 既存挙動: Claude Stop hook の flush barrier は grok 以外 (codex: 含む) に適用される。
|
|
17
|
+
waitsForStopTranscriptFlush: true,
|
|
18
|
+
deliverHandoffInjection({ text, stdout = process.stdout }) {
|
|
19
|
+
stdout.write(text + '\n');
|
|
20
|
+
return { delivered: true };
|
|
21
|
+
},
|
|
22
|
+
resolveCommandPrompt({ prompt }) {
|
|
23
|
+
return prompt;
|
|
24
|
+
},
|
|
25
|
+
afterBatonWrite() {
|
|
26
|
+
return { launched: false };
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hosts/grok.mjs — Grok host 境界 (envelope 正規化 + hook adapter)
|
|
3
|
+
*
|
|
4
|
+
* Grok sends Claude-compatible hook commands with a camelCase wire
|
|
5
|
+
* (sessionId, hookEventName) and no session_id. Throughline treats that
|
|
6
|
+
* envelope as host=grok: normalize to the Claude snake_case contract and
|
|
7
|
+
* prefix session ids so they never mix with Claude predecessor search.
|
|
8
|
+
*
|
|
9
|
+
* Grok 固有の挙動 (v0.10.0 / ADR 0021):
|
|
10
|
+
* - UserPromptSubmit stdout はモデルへ渡らないため、引き継ぎ注入は
|
|
11
|
+
* chat_history.jsonl への直接書き込みで行う
|
|
12
|
+
* - hook prompt は `<user_query>` 包装のため、裸の slash command 判定には
|
|
13
|
+
* chat_history の最新 user 発話を使う
|
|
14
|
+
* - `/tl` 成功後だけ grok-continue で後継の対話 grok を立てる
|
|
15
|
+
* - Stop hook の transcript flush barrier は Claude transcript 専用のため使わない
|
|
16
|
+
*/
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
|
|
20
|
+
import { GROK_HOST, GROK_SESSION_PREFIX, grokBareSessionId, isGrokSessionId } from './identity.mjs';
|
|
21
|
+
import { injectGrokHandoffContext } from '../grok-history-inject.mjs';
|
|
22
|
+
import { readTranscript } from '../transcript-reader.mjs';
|
|
23
|
+
import { run as runGrokContinue } from '../cli/grok-continue.mjs';
|
|
24
|
+
|
|
25
|
+
export { GROK_SESSION_PREFIX, grokBareSessionId };
|
|
26
|
+
|
|
27
|
+
export function isGrokEnvelope(payload) {
|
|
28
|
+
return payload !== null
|
|
29
|
+
&& typeof payload === 'object'
|
|
30
|
+
&& typeof payload.sessionId === 'string'
|
|
31
|
+
&& payload.sessionId.length > 0
|
|
32
|
+
&& typeof payload.hookEventName === 'string'
|
|
33
|
+
&& payload.hookEventName.length > 0
|
|
34
|
+
&& !Object.hasOwn(payload, 'session_id');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function deriveGrokChatHistoryPath(projectPath, sessionId, { home = homedir() } = {}) {
|
|
38
|
+
const bare = grokBareSessionId(sessionId);
|
|
39
|
+
if (!projectPath || !bare) return null;
|
|
40
|
+
return join(home, '.grok', 'sessions', encodeURIComponent(projectPath), bare, 'chat_history.jsonl');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeGrokHookPayload(payload, { home = homedir() } = {}) {
|
|
44
|
+
if (!isGrokEnvelope(payload)) return payload;
|
|
45
|
+
const cwd = typeof payload.cwd === 'string' && payload.cwd.length > 0
|
|
46
|
+
? payload.cwd
|
|
47
|
+
: (typeof payload.workspaceRoot === 'string' ? payload.workspaceRoot : undefined);
|
|
48
|
+
// Live Grok Stop sets transcriptPath to updates.jsonl (sessionUpdate frames,
|
|
49
|
+
// no user/assistant rows). L2 lives in chat_history.jsonl only.
|
|
50
|
+
const transcriptPath = deriveGrokChatHistoryPath(cwd, payload.sessionId, { home });
|
|
51
|
+
return {
|
|
52
|
+
...payload,
|
|
53
|
+
session_id: `${GROK_SESSION_PREFIX}${payload.sessionId}`,
|
|
54
|
+
cwd,
|
|
55
|
+
source: payload.source,
|
|
56
|
+
prompt: payload.prompt,
|
|
57
|
+
hook_event_name: payload.hookEventName,
|
|
58
|
+
transcript_path: transcriptPath,
|
|
59
|
+
last_assistant_message: payload.lastAssistantMessage,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function lastUserPromptText(transcriptPath) {
|
|
64
|
+
const turns = readTranscript(transcriptPath);
|
|
65
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
66
|
+
if (turns[i].role === 'user') return turns[i].content;
|
|
67
|
+
}
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const grokHostAdapter = Object.freeze({
|
|
72
|
+
host: GROK_HOST,
|
|
73
|
+
matchesSessionId: isGrokSessionId,
|
|
74
|
+
// Claude Stop transcript flush barrier は Claude transcript の完了行を待つ機構。
|
|
75
|
+
// Grok の chat_history には適用しない (既存挙動)。
|
|
76
|
+
waitsForStopTranscriptFlush: false,
|
|
77
|
+
// Grok は UserPromptSubmit stdout をモデルへ渡さないため chat_history に直接注入する。
|
|
78
|
+
deliverHandoffInjection({ payload, text }) {
|
|
79
|
+
const injected = injectGrokHandoffContext(payload.transcript_path, text);
|
|
80
|
+
return injected.injected
|
|
81
|
+
? { delivered: true }
|
|
82
|
+
: { delivered: false, reason: `grok chat_history inject skipped: ${injected.reason}` };
|
|
83
|
+
},
|
|
84
|
+
// Grok の hook prompt は `<user_query>` 包装 + skill 本文のため、裸の /tl 判定は
|
|
85
|
+
// chat_history の最新 user 発話へ fallback する。
|
|
86
|
+
resolveCommandPrompt({ prompt, payload, isCommandPrompt }) {
|
|
87
|
+
if (isCommandPrompt(prompt)) return prompt;
|
|
88
|
+
return lastUserPromptText(payload.transcript_path);
|
|
89
|
+
},
|
|
90
|
+
// Grok `/tl` 成功後だけ、源セッションの project_path で後継の対話 grok を立てる。
|
|
91
|
+
afterBatonWrite({ trigger, sessionId, cwd, continueRun = runGrokContinue }) {
|
|
92
|
+
if (trigger !== 'tl') return { launched: false };
|
|
93
|
+
const code = continueRun(['--session', sessionId], { cwd });
|
|
94
|
+
return { launched: true, exitCode: code };
|
|
95
|
+
},
|
|
96
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hosts/identity.mjs — ベンダー (hook host) 識別の唯一の正本
|
|
3
|
+
*
|
|
4
|
+
* Throughline は Claude / Codex / Grok の 3 hook host を同じ SQLite に保存する。
|
|
5
|
+
* host の見分け方は session_id prefix だけであり、その prefix 定義と判定関数を
|
|
6
|
+
* このファイルに一元化する。共有コード (hook 入口・monitor・state-file・
|
|
7
|
+
* predecessor 検索) は文字列リテラルを直接持たず、必ずここを参照する。
|
|
8
|
+
*
|
|
9
|
+
* 新しい host を足す場合はここへ prefix / host 名を追加し、
|
|
10
|
+
* `src/hosts/<host>.mjs` に adapter を実装する。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const CLAUDE_HOST = 'claude';
|
|
14
|
+
export const CODEX_HOST = 'codex';
|
|
15
|
+
export const GROK_HOST = 'grok';
|
|
16
|
+
|
|
17
|
+
export const CODEX_SESSION_PREFIX = 'codex:';
|
|
18
|
+
export const GROK_SESSION_PREFIX = 'grok:';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Claude session は prefix を持たない。auto handoff の前任検索 (Claude 専用) は
|
|
22
|
+
* この一覧の prefix を持つ session を除外する。
|
|
23
|
+
*/
|
|
24
|
+
export const NON_CLAUDE_SESSION_PREFIXES = Object.freeze([
|
|
25
|
+
CODEX_SESSION_PREFIX,
|
|
26
|
+
GROK_SESSION_PREFIX,
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* state ファイルに保存できる host 値。Grok session は Claude 互換 hook 経路で
|
|
31
|
+
* 保存されるため state 上は 'claude' として扱う (既存挙動)。
|
|
32
|
+
*/
|
|
33
|
+
export const KNOWN_STATE_HOSTS = Object.freeze([CLAUDE_HOST, CODEX_HOST]);
|
|
34
|
+
|
|
35
|
+
export function isCodexSessionId(sessionId) {
|
|
36
|
+
return typeof sessionId === 'string' && sessionId.startsWith(CODEX_SESSION_PREFIX);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isGrokSessionId(sessionId) {
|
|
40
|
+
return typeof sessionId === 'string' && sessionId.startsWith(GROK_SESSION_PREFIX);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* session_id から hook host を返す。prefix なしは Claude。
|
|
45
|
+
* @param {string} sessionId
|
|
46
|
+
* @returns {'claude'|'codex'|'grok'}
|
|
47
|
+
*/
|
|
48
|
+
export function hostOfSessionId(sessionId) {
|
|
49
|
+
if (isCodexSessionId(sessionId)) return CODEX_HOST;
|
|
50
|
+
if (isGrokSessionId(sessionId)) return GROK_HOST;
|
|
51
|
+
return CLAUDE_HOST;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildCodexThroughlineSessionId(threadId) {
|
|
55
|
+
if (typeof threadId !== 'string' || threadId.trim().length === 0) {
|
|
56
|
+
throw new Error('threadId is required');
|
|
57
|
+
}
|
|
58
|
+
return `${CODEX_SESSION_PREFIX}${threadId.trim()}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function codexSessionIdToThreadId(sessionId) {
|
|
62
|
+
if (!isCodexSessionId(sessionId)) return null;
|
|
63
|
+
return sessionId.slice(CODEX_SESSION_PREFIX.length) || null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function grokBareSessionId(sessionId) {
|
|
67
|
+
if (typeof sessionId !== 'string' || sessionId.length === 0) return null;
|
|
68
|
+
return sessionId.startsWith(GROK_SESSION_PREFIX)
|
|
69
|
+
? sessionId.slice(GROK_SESSION_PREFIX.length)
|
|
70
|
+
: sessionId;
|
|
71
|
+
}
|