throughline 0.10.0 → 0.10.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.
- package/CHANGELOG.md +38 -1
- package/README.ja.md +1 -1
- package/README.md +2 -2
- package/docs/16_readonly_handoff_context_plan.md +2 -2
- package/package.json +1 -1
- package/src/auditor-context.mjs +4 -3
- package/src/cli/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/doctor.mjs +7 -6
- package/src/cli/grok-continue.mjs +8 -21
- package/src/cli/runtime-errors.test.mjs +1 -1
- package/src/cli/trim.mjs +3 -2
- package/src/codex-auto-refresh.mjs +3 -3
- package/src/codex-capture.mjs +14 -18
- package/src/codex-handoff-model-smoke.mjs +1 -1
- package/src/codex-sidecar.mjs +3 -2
- package/src/completed-turn-receipts.mjs +6 -33
- 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/app-dirs.mjs +25 -0
- package/src/os/macos-terminal.mjs +44 -0
- package/src/os/open-url.mjs +16 -0
- package/src/os/paths.mjs +16 -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 +9 -47
- 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/trim-model.mjs +6 -5
- 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,39 @@ shipped to npm but were not individually tagged on GitHub.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.10.2] — 2026-08-24
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- 挙動不変のOS/harness層集約リファクタ(harness用語統一campaignの分離規約):
|
|
18
|
+
- LOCALAPPDATA / XDG_CONFIG_HOME / XDG_STATE_HOME のベースディレクトリ組み立てを
|
|
19
|
+
新設`src/os/app-dirs.mjs`へ一本化(runtime-error-store / completed-turn-receipts
|
|
20
|
+
に3重実装されていた)。
|
|
21
|
+
- hostリテラル比較(`'claude'`/`'codex'`)6ファイルを`src/hosts/identity.mjs`の
|
|
22
|
+
定数importへ統一し、「識別の唯一の正本」宣言と実装を一致させた。
|
|
23
|
+
- codex-auto-refreshの独自パス正規化を`src/os/paths.mjs`の
|
|
24
|
+
`foldPathCaseForPlatform`へ、codex-sidecarのwin32判定を同`isWin32Platform`へ委譲。
|
|
25
|
+
- `hosts/identity.mjs`の分類語をvendorからharnessへ更新(用語のみ)。
|
|
26
|
+
|
|
27
|
+
## [0.10.1] — 2026-08-23
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- Internal refactor with no behavior change. Vendor (hook host) identity and
|
|
32
|
+
per-host behavior moved into `src/hosts/` — `hosts/identity.mjs` is the
|
|
33
|
+
single source of the `codex:` / `grok:` session prefixes (previously
|
|
34
|
+
duplicated across four files), and the shared hook entrypoints
|
|
35
|
+
(`session-start` / `prompt-submit` / `process-turn`) now branch through
|
|
36
|
+
`hosts/{claude,codex,grok}.mjs` adapters instead of inline host checks.
|
|
37
|
+
`src/hook-envelope.mjs` is merged into `hosts/grok.mjs`.
|
|
38
|
+
- OS-specific code moved into `src/os/` — the Windows owner-only ACL
|
|
39
|
+
PowerShell implementation that was duplicated verbatim in
|
|
40
|
+
`runtime-error-store` and `completed-turn-receipts` is now the single
|
|
41
|
+
`os/windows-acl.mjs`, alongside macOS Terminal launch, OS URL open,
|
|
42
|
+
shell/AppleScript quoting, Windows path case folding, and the portable
|
|
43
|
+
spawn helper. DB schema, injection contract, and every CLI surface are
|
|
44
|
+
unchanged; full regression is 761 pass / 0 fail.
|
|
45
|
+
|
|
13
46
|
## [0.10.0] — 2026-08-17
|
|
14
47
|
|
|
15
48
|
### Added
|
|
@@ -1271,7 +1304,11 @@ two attempts, instrument first instead of patching again.
|
|
|
1271
1304
|
|
|
1272
1305
|
---
|
|
1273
1306
|
|
|
1274
|
-
[Unreleased]: https://github.com/kitepon-rgb/Throughline/compare/v0.
|
|
1307
|
+
[Unreleased]: https://github.com/kitepon-rgb/Throughline/compare/v0.10.1...HEAD
|
|
1308
|
+
[0.10.1]: https://github.com/kitepon-rgb/Throughline/compare/v0.10.0...v0.10.1
|
|
1309
|
+
[0.10.0]: https://github.com/kitepon-rgb/Throughline/compare/v0.9.1...v0.10.0
|
|
1310
|
+
[0.9.1]: https://github.com/kitepon-rgb/Throughline/compare/v0.9.0...v0.9.1
|
|
1311
|
+
[0.9.0]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.9...v0.9.0
|
|
1275
1312
|
[0.8.9]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.8...v0.8.9
|
|
1276
1313
|
[0.8.8]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.7...v0.8.8
|
|
1277
1314
|
[0.8.7]: https://github.com/kitepon-rgb/Throughline/compare/v0.8.6...v0.8.7
|
package/README.ja.md
CHANGED
|
@@ -390,7 +390,7 @@ throughline handoff-context --session codex:<thread-id> --json
|
|
|
390
390
|
成功時の`throughline.handoff_context.v1`は`schema`、`status`、`sessionId`、`context`だけを返す。
|
|
391
391
|
`context`はSessionStartと同じ予算付き継承文脈で、DB作成・migration・baton消費・session merge・
|
|
392
392
|
latest session推測・`sessions.merged_into`変更・L1/L2/L3 rowの所属変更は行わない。AItermは任意の
|
|
393
|
-
別
|
|
393
|
+
別harness portable forkでこの境界を使う。Observer feedはcompleted-turn projectionであり代替ではない。
|
|
394
394
|
|
|
395
395
|
スラッシュコマンド (Claude Code 内でユーザーが叩く):
|
|
396
396
|
|
package/README.md
CHANGED
|
@@ -834,7 +834,7 @@ local-only contracts.
|
|
|
834
834
|
| `throughline runtime-errors reopen <fingerprint> --json` | Explicitly reopen a resolved aggregate without fabricating a new occurrence |
|
|
835
835
|
| `throughline runtime-errors compact --json` | Remove only acknowledged, resolved aggregates after retention; open or unacknowledged records remain |
|
|
836
836
|
| `throughline handoff-preview --session <id>` | Print a Codex-facing `throughline_handoff` JSON projection |
|
|
837
|
-
| `throughline handoff-context --session <id> --json` | Print the exact SessionStart inheritance context as versioned JSON without moving memory rows or changing `sessions.merged_into`; intended for local launchers that need portable cross-
|
|
837
|
+
| `throughline handoff-context --session <id> --json` | Print the exact SessionStart inheritance context as versioned JSON without moving memory rows or changing `sessions.merged_into`; intended for local launchers that need portable cross-harness context |
|
|
838
838
|
| `throughline grok-continue --session <id>` | Spawn a person-facing Grok seat whose first user text is the handoff-context body. cwd is the source session `project_path`. Does not spawn without ready context. No `--rules`. macOS Terminal only |
|
|
839
839
|
| `throughline codex-capture --codex-thread-id <id>` | Capture active Codex rollout turns into a `codex:<thread_id>` DB session |
|
|
840
840
|
| `throughline codex-summarize --session codex:<id>` | Summarize captured Codex L2 into L1 with the Codex CLI backend |
|
|
@@ -872,7 +872,7 @@ The successful `throughline.handoff_context.v1` object contains only `schema`,
|
|
|
872
872
|
inheritance text used by SessionStart. The command does not create or migrate a
|
|
873
873
|
database, consume a baton, merge sessions, infer a latest session, change
|
|
874
874
|
`sessions.merged_into`, or reassign L1/L2/L3 rows. AIterm uses this boundary for
|
|
875
|
-
its optional cross-
|
|
875
|
+
its optional cross-harness portable fork; the Observer feed is a separate
|
|
876
876
|
completed-turn projection and is not a substitute.
|
|
877
877
|
|
|
878
878
|
Slash commands (invoked by the user in Claude Code):
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
通常 handoff が使う `buildBudgetedResumeContext` の出力を、DB 所有権を変えずにローカルプログラムが
|
|
6
6
|
取得できる read-only CLI として公開する。最初の consumer は AIterm の portable fork だが、I/F は
|
|
7
|
-
特定 consumer や
|
|
7
|
+
特定 consumer や harness に依存させない。
|
|
8
8
|
|
|
9
9
|
実行 ToDo、依存、状態、完了証拠の正本は Lattice plan `readonly-handoff-context` とする。
|
|
10
10
|
|
|
11
11
|
## 完了
|
|
12
12
|
|
|
13
13
|
2026-08-04に`throughline@0.9.0`としてnpm、tag、GitHub Release、global installまで公開した。
|
|
14
|
-
focused契約testと全回帰は729 pass/1 skip/0 fail。AIterm v0.23.0の代表cross-
|
|
14
|
+
focused契約testと全回帰は729 pass/1 skip/0 fail。AIterm v0.23.0の代表cross-harness smokeでは
|
|
15
15
|
Codex source memoryをClaudeへ注入し、前後でsource session、`sessions.merged_into`、L1/L2/L3 row所属が
|
|
16
16
|
完全一致することを確認した。公開後の現行ドキュメント全域監査は、Latticeの終端ToDoを再openして
|
|
17
17
|
README、作業者入口、配布Codex skill、docs索引、計画、CHANGELOGへ同期した。変更Markdownの
|
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 { CLAUDE_HOST, CODEX_HOST, 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';
|
|
@@ -23,7 +24,7 @@ export function defaultAuditorContextDbPath() {
|
|
|
23
24
|
export function deriveAuditorFreshnessExpectation({ host, transcriptPath, sessionId } = {}) {
|
|
24
25
|
assertNonEmptyString(transcriptPath, 'transcriptPath');
|
|
25
26
|
assertNonEmptyString(sessionId, 'sessionId');
|
|
26
|
-
if (host ===
|
|
27
|
+
if (host === CLAUDE_HOST) {
|
|
27
28
|
const latest = getLogicalTurnGroups(transcriptPath).at(-1);
|
|
28
29
|
if (!latest) return null;
|
|
29
30
|
return {
|
|
@@ -33,7 +34,7 @@ export function deriveAuditorFreshnessExpectation({ host, transcriptPath, sessio
|
|
|
33
34
|
expectedAssistantSha256: hashAuditorBody(latest.representative.content),
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
|
-
if (host ===
|
|
37
|
+
if (host === CODEX_HOST) {
|
|
37
38
|
const parsed = parseCodexRolloutFile(transcriptPath, { includeInFlightTurn: false });
|
|
38
39
|
const rows = buildBodyRowsFromActiveTurns(parsed.activeTurns, { sessionId, now: 0 });
|
|
39
40
|
const latestTurnNumber = rows.reduce((max, row) => Math.max(max, row.turnNumber), 0);
|
|
@@ -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) &&
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CLAUDE_HOST, CODEX_HOST } from '../hosts/identity.mjs';
|
|
1
2
|
import {
|
|
2
3
|
AUDITOR_CONTEXT_SCHEMA,
|
|
3
4
|
deriveAuditorFreshnessExpectation,
|
|
@@ -53,7 +54,7 @@ export function parseArgs(argv = []) {
|
|
|
53
54
|
else if (arg === '--max-body-chars') out.maxBodyChars = parsePositiveInteger(value);
|
|
54
55
|
else if (arg === '--max-total-chars') out.maxTotalChars = parsePositiveInteger(value);
|
|
55
56
|
else if (arg === '--db') out.dbPath = value;
|
|
56
|
-
else if (arg === '--host' && (value ===
|
|
57
|
+
else if (arg === '--host' && (value === CLAUDE_HOST || value === CODEX_HOST)) out.host = value;
|
|
57
58
|
else if (arg === '--transcript') out.transcriptPath = value;
|
|
58
59
|
else throw new TypeError('unknown option');
|
|
59
60
|
}
|
|
@@ -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',
|
package/src/cli/doctor.mjs
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { existsSync, accessSync, readFileSync, constants, readdirSync, statSync } from 'node:fs';
|
|
17
|
+
import { CODEX_HOST } from '../hosts/identity.mjs';
|
|
17
18
|
import { join, dirname } from 'node:path';
|
|
18
19
|
import { homedir } from 'node:os';
|
|
19
20
|
import { execSync } from 'node:child_process';
|
|
@@ -302,9 +303,9 @@ function runTrimDiagnosis(
|
|
|
302
303
|
) {
|
|
303
304
|
const info = describeTrimHost(host);
|
|
304
305
|
const codexIdentity =
|
|
305
|
-
info.host ===
|
|
306
|
+
info.host === CODEX_HOST ? resolveCodexThreadIdentity({ codexThreadId: null }, env) : null;
|
|
306
307
|
const hostPrimitiveDiagnosis =
|
|
307
|
-
info.host ===
|
|
308
|
+
info.host === CODEX_HOST ? readCodexHostPrimitiveDiagnosis({ env, auditRunner }) : null;
|
|
308
309
|
console.log(`${BOLD}[Trim]${RESET}\n`);
|
|
309
310
|
console.log(` host: ${info.host}`);
|
|
310
311
|
console.log(` default keep-recent: ${DEFAULT_TRIM_KEEP_RECENT}`);
|
|
@@ -330,19 +331,19 @@ function runTrimDiagnosis(
|
|
|
330
331
|
}
|
|
331
332
|
console.log('');
|
|
332
333
|
console.log(' dry-run command:');
|
|
333
|
-
if (info.host ===
|
|
334
|
+
if (info.host === CODEX_HOST && !codexIdentity?.codexThreadId) {
|
|
334
335
|
console.log(' throughline trim --dry-run --host codex --codex-thread-id <id>');
|
|
335
336
|
console.log(' throughline trim --preflight --host codex --codex-thread-id <id>');
|
|
336
|
-
} else if (info.host ===
|
|
337
|
+
} else if (info.host === CODEX_HOST) {
|
|
337
338
|
console.log(' throughline trim --dry-run --host codex');
|
|
338
339
|
console.log(' throughline trim --preflight --host codex');
|
|
339
340
|
} else {
|
|
340
341
|
console.log(` throughline trim --dry-run --host ${info.host}`);
|
|
341
342
|
}
|
|
342
|
-
if (info.host ===
|
|
343
|
+
if (info.host === CODEX_HOST) {
|
|
343
344
|
console.log(' throughline trim --execute --host codex');
|
|
344
345
|
}
|
|
345
|
-
if (info.host ===
|
|
346
|
+
if (info.host === CODEX_HOST) {
|
|
346
347
|
const sessionId = codexIdentity?.codexThreadId
|
|
347
348
|
? `codex:${codexIdentity.codexThreadId}`
|
|
348
349
|
: 'codex:<thread-id>';
|
|
@@ -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/cli/trim.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runCodexTrimExecution, runCodexTrimPreflight } from '../codex-app-server.mjs';
|
|
2
|
+
import { CODEX_HOST } from '../hosts/identity.mjs';
|
|
2
3
|
import {
|
|
3
4
|
buildCodexRolloutTrimSource,
|
|
4
5
|
parseCodexRolloutFile,
|
|
@@ -122,7 +123,7 @@ export async function run(args) {
|
|
|
122
123
|
const inflightMemo = parsed.memoStdin ? await readStdin() : null;
|
|
123
124
|
const db = getDb();
|
|
124
125
|
const trimSource =
|
|
125
|
-
parsed.host ===
|
|
126
|
+
parsed.host === CODEX_HOST && parsed.codexThreadId
|
|
126
127
|
? buildCodexRolloutTrimSource({
|
|
127
128
|
threadId: parsed.codexThreadId,
|
|
128
129
|
projectPath: process.cwd(),
|
|
@@ -376,7 +377,7 @@ async function runPreflight(parsed, plan) {
|
|
|
376
377
|
}
|
|
377
378
|
|
|
378
379
|
function validateCodexAction(parsed, plan, action) {
|
|
379
|
-
if (parsed.host !==
|
|
380
|
+
if (parsed.host !== CODEX_HOST) {
|
|
380
381
|
return {
|
|
381
382
|
status: `${action}-refused`,
|
|
382
383
|
reason: `${action}_requires_codex_host`,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import { foldPathCaseForPlatform } from './os/paths.mjs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
3
4
|
import { join, resolve } from 'node:path';
|
|
4
5
|
|
|
5
6
|
import { runCodexTrimExecution } from './codex-app-server.mjs';
|
|
@@ -509,8 +510,7 @@ function normalizeProjectPath(value) {
|
|
|
509
510
|
if (typeof value !== 'string' || value.length === 0) return '';
|
|
510
511
|
let result = resolve(value).replace(/\\/g, '/');
|
|
511
512
|
if (result.length > 1 && result.endsWith('/')) result = result.slice(0, -1);
|
|
512
|
-
|
|
513
|
-
return result;
|
|
513
|
+
return foldPathCaseForPlatform(result);
|
|
514
514
|
}
|
|
515
515
|
|
|
516
516
|
function numberOrNull(value) {
|
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
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { spawnPortableSync } from './portable-spawn-sync.mjs';
|
|
1
|
+
import { spawnPortableSync } from './os/portable-spawn-sync.mjs';
|
|
2
|
+
import { isWin32Platform } from './os/paths.mjs';
|
|
2
3
|
|
|
3
4
|
export const CODEX_SIDECAR_WORKFLOWS = Object.freeze([
|
|
4
5
|
'review',
|
|
@@ -23,7 +24,7 @@ function resolveCommand({ command, env }) {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export function shouldShellWrapSidecarCommand(platform = process.platform) {
|
|
26
|
-
return platform
|
|
27
|
+
return isWin32Platform(platform);
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export function runCodexSidecarCommand(command, args, options = {}) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { CLAUDE_HOST } from './hosts/identity.mjs';
|
|
2
3
|
import {
|
|
3
4
|
chmodSync,
|
|
4
5
|
lstatSync,
|
|
@@ -8,12 +9,12 @@ import {
|
|
|
8
9
|
rmSync,
|
|
9
10
|
writeFileSync,
|
|
10
11
|
} from 'node:fs';
|
|
11
|
-
import childProcess from 'node:child_process';
|
|
12
|
-
import { homedir, platform as hostPlatform } 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
|
+
import { windowsLocalAppData, xdgStateHome } from './os/app-dirs.mjs';
|
|
17
18
|
|
|
18
19
|
export const COMPLETED_TURN_RECEIPT_STORE_SCHEMA = 'throughline.completed_turn_receipts.v1';
|
|
19
20
|
export const COMPLETED_TURN_RECEIPT_SCHEMA_VERSION = '1.0';
|
|
@@ -21,15 +22,13 @@ export const COMPLETED_TURN_RECEIPT_LIMIT = 256;
|
|
|
21
22
|
|
|
22
23
|
const PRIVATE_DIRECTORY_CAPABILITY = Symbol('throughline.completed-turn-receipt-directory');
|
|
23
24
|
// CI実測でPowerShellコールドスタートが3.0〜3.2秒に達しflakeしたため15秒 (run 29586852389 / 29628634501)
|
|
24
|
-
const WINDOWS_ACL_TIMEOUT_MS = 15_000;
|
|
25
25
|
|
|
26
26
|
export function defaultCompletedTurnReceiptStorePath(projectSha256, env = process.env) {
|
|
27
27
|
if (!isSha256(projectSha256)) throw new TypeError('projectSha256 must be a SHA-256 digest');
|
|
28
|
-
const home = env.HOME || env.USERPROFILE || homedir();
|
|
29
28
|
if (isWindows(env)) {
|
|
30
|
-
return join(env
|
|
29
|
+
return join(windowsLocalAppData(env), 'throughline', 'completed-turn-receipts', `${projectSha256}.json`);
|
|
31
30
|
}
|
|
32
|
-
return join(env
|
|
31
|
+
return join(xdgStateHome(env), 'throughline', 'completed-turn-receipts', `${projectSha256}.json`);
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
/**
|
|
@@ -274,7 +273,7 @@ function validateReceipt(receipt, nextSequence) {
|
|
|
274
273
|
];
|
|
275
274
|
if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt) ||
|
|
276
275
|
!exactKeys(receipt, keys) || receipt.schema_version !== COMPLETED_TURN_RECEIPT_SCHEMA_VERSION ||
|
|
277
|
-
receipt.host !==
|
|
276
|
+
receipt.host !== CLAUDE_HOST || !isSha256(receipt.project_sha256) ||
|
|
278
277
|
!isIdentity(receipt.target_session_id) || !isIdentity(receipt.origin_session_id) ||
|
|
279
278
|
!isSha256(receipt.user_sha256) || !isSha256(receipt.assistant_sha256) ||
|
|
280
279
|
!Number.isSafeInteger(receipt.completed_at) || receipt.completed_at < 0 ||
|
|
@@ -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
|
+
});
|