@wooojin/forgen 0.4.1 → 0.4.3
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/.claude-plugin/plugin.json +5 -5
- package/CHANGELOG.md +164 -15
- package/CONTRIBUTING.md +2 -2
- package/README.ja.md +17 -9
- package/README.ko.md +20 -12
- package/README.md +46 -12
- package/README.zh.md +17 -9
- package/assets/README.md +86 -0
- package/assets/architecture.svg +100 -0
- package/assets/banner.png +0 -0
- package/assets/banner.svg +53 -0
- package/assets/demo/01-install.gif +0 -0
- package/assets/demo/01-install.tape +54 -0
- package/assets/demo/02-compound-learning.gif +0 -0
- package/assets/demo/02-compound-learning.tape +50 -0
- package/assets/demo/03-forge-personalization.gif +0 -0
- package/assets/demo/03-forge-personalization.tape +64 -0
- package/assets/demo/before-after.gif +0 -0
- package/assets/demo/before-after.tape +98 -0
- package/assets/demo-preview.svg +96 -0
- package/assets/icon.png +0 -0
- package/{hooks → assets/shared}/hook-registry.json +2 -1
- package/dist/cli.js +78 -6
- package/dist/core/auto-compound-runner.js +62 -38
- package/dist/core/behavior-classifier.d.ts +28 -0
- package/dist/core/behavior-classifier.js +46 -0
- package/dist/core/dashboard.d.ts +7 -0
- package/dist/core/dashboard.js +32 -0
- package/dist/core/doctor.js +92 -0
- package/dist/core/git-stats.d.ts +36 -0
- package/dist/core/git-stats.js +79 -0
- package/dist/core/harness.d.ts +1 -1
- package/dist/core/harness.js +27 -20
- package/dist/core/host-detect.d.ts +42 -0
- package/dist/core/host-detect.js +68 -0
- package/dist/core/installer.js +2 -2
- package/dist/core/migrate-cli.d.ts +1 -0
- package/dist/core/migrate-cli.js +19 -0
- package/dist/core/migrate-evidence-host.d.ts +36 -0
- package/dist/core/migrate-evidence-host.js +49 -0
- package/dist/core/settings-injector.js +4 -2
- package/dist/core/spawn.d.ts +1 -1
- package/dist/core/spawn.js +4 -11
- package/dist/core/stats-cli.js +12 -0
- package/dist/core/trust-layer-intent.d.ts +35 -0
- package/dist/core/trust-layer-intent.js +30 -0
- package/dist/core/types.d.ts +1 -1
- package/dist/engine/compound-extractor.js +7 -9
- package/dist/engine/learn-cli.js +4 -2
- package/dist/engine/lifecycle/bypass-detector.d.ts +6 -1
- package/dist/engine/lifecycle/bypass-detector.js +57 -5
- package/dist/fgx.js +2 -1
- package/dist/forge/evidence-processor.js +12 -0
- package/dist/forge/onboarding.d.ts +3 -2
- package/dist/forge/onboarding.js +3 -2
- package/dist/hooks/db-guard.js +3 -3
- package/dist/hooks/forge-loop-progress.d.ts +9 -0
- package/dist/hooks/forge-loop-progress.js +38 -0
- package/dist/hooks/hook-registry.js +1 -1
- package/dist/hooks/hooks-generator.d.ts +15 -1
- package/dist/hooks/hooks-generator.js +18 -16
- package/dist/hooks/keyword-detector.js +1 -1
- package/dist/hooks/post-tool-use.d.ts +1 -1
- package/dist/hooks/post-tool-use.js +13 -4
- package/dist/hooks/pre-compact.js +1 -1
- package/dist/hooks/pre-tool-use.js +4 -4
- package/dist/hooks/rate-limiter.js +2 -2
- package/dist/hooks/session-recovery.js +11 -0
- package/dist/hooks/shared/blocking-allowlist.d.ts +28 -0
- package/dist/hooks/shared/blocking-allowlist.js +38 -0
- package/dist/hooks/shared/forge-loop-state.d.ts +36 -0
- package/dist/hooks/shared/forge-loop-state.js +116 -0
- package/dist/hooks/shared/hook-response.d.ts +18 -0
- package/dist/hooks/shared/hook-response.js +31 -0
- package/dist/hooks/skill-injector.js +1 -1
- package/dist/hooks/stop-guard.js +15 -0
- package/dist/host/capabilities-claude.d.ts +8 -0
- package/dist/host/capabilities-claude.js +46 -0
- package/dist/host/capabilities-codex.d.ts +11 -0
- package/dist/host/capabilities-codex.js +50 -0
- package/dist/host/capabilities-registry.d.ts +11 -0
- package/dist/host/capabilities-registry.js +30 -0
- package/dist/host/codex-adapter.d.ts +8 -5
- package/dist/host/codex-adapter.js +10 -82
- package/dist/host/codex-output-parser.d.ts +39 -0
- package/dist/host/codex-output-parser.js +75 -0
- package/dist/host/exec-host.d.ts +54 -0
- package/dist/host/exec-host.js +92 -0
- package/dist/host/host-runtime.d.ts +37 -0
- package/dist/host/host-runtime.js +51 -0
- package/dist/host/install-claude.d.ts +35 -0
- package/dist/host/install-claude.js +238 -0
- package/dist/host/install-codex.d.ts +44 -0
- package/dist/host/install-codex.js +276 -0
- package/dist/host/install-orchestrator.d.ts +34 -0
- package/dist/host/install-orchestrator.js +126 -0
- package/dist/host/invoke-agent.d.ts +27 -0
- package/dist/host/invoke-agent.js +115 -0
- package/dist/host/parity-harness.d.ts +62 -0
- package/dist/host/parity-harness.js +283 -0
- package/dist/host/projection.d.ts +35 -0
- package/dist/host/projection.js +126 -0
- package/dist/mcp/server.js +11 -0
- package/dist/mcp/tools.js +47 -0
- package/dist/services/session.d.ts +6 -3
- package/dist/services/session.js +33 -4
- package/dist/store/evidence-store.d.ts +1 -0
- package/dist/store/evidence-store.js +34 -3
- package/dist/store/host-mismatch.d.ts +42 -0
- package/dist/store/host-mismatch.js +65 -0
- package/dist/store/profile-store.d.ts +29 -0
- package/dist/store/profile-store.js +53 -0
- package/dist/store/types.d.ts +13 -0
- package/hooks/hooks.json +6 -1
- package/package.json +6 -4
- package/plugin.json +4 -4
- package/scripts/postinstall.js +100 -25
- /package/{agents → assets/claude/agents}/analyst.md +0 -0
- /package/{agents → assets/claude/agents}/architect.md +0 -0
- /package/{agents → assets/claude/agents}/code-reviewer.md +0 -0
- /package/{agents → assets/claude/agents}/critic.md +0 -0
- /package/{agents → assets/claude/agents}/debugger.md +0 -0
- /package/{agents → assets/claude/agents}/designer.md +0 -0
- /package/{agents → assets/claude/agents}/executor.md +0 -0
- /package/{agents → assets/claude/agents}/explore.md +0 -0
- /package/{agents → assets/claude/agents}/git-master.md +0 -0
- /package/{agents → assets/claude/agents}/planner.md +0 -0
- /package/{agents → assets/claude/agents}/solution-evolver.md +0 -0
- /package/{agents → assets/claude/agents}/test-engineer.md +0 -0
- /package/{agents → assets/claude/agents}/verifier.md +0 -0
- /package/{commands → assets/claude/commands}/architecture-decision.md +0 -0
- /package/{commands → assets/claude/commands}/calibrate.md +0 -0
- /package/{commands → assets/claude/commands}/code-review.md +0 -0
- /package/{commands → assets/claude/commands}/compound.md +0 -0
- /package/{commands → assets/claude/commands}/deep-interview.md +0 -0
- /package/{commands → assets/claude/commands}/docker.md +0 -0
- /package/{commands → assets/claude/commands}/forge-loop.md +0 -0
- /package/{commands → assets/claude/commands}/learn.md +0 -0
- /package/{commands → assets/claude/commands}/retro.md +0 -0
- /package/{commands → assets/claude/commands}/ship.md +0 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forge Loop State — RC6 가드 (US-M1)
|
|
3
|
+
*
|
|
4
|
+
* 직전 forge-loop 의 findings 또는 진행 중 stories 를 ≤1KB 요약으로 렌더한다.
|
|
5
|
+
* SessionStart 와 UserPromptSubmit 두 hook 이 공유하는 단일 진입점.
|
|
6
|
+
*
|
|
7
|
+
* RC6 자기증거: 본 세션 R1 에서 head -80 으로 forge-loop.json 을 읽어 findings
|
|
8
|
+
* 8줄(line 92~99)이 잘렸음. 결과적으로 직전 결론을 컨텍스트에 못 가져 같은
|
|
9
|
+
* 가설을 재발. 이 모듈은 그 회귀를 시스템 레벨에서 차단한다.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import { STATE_DIR } from '../../core/paths.js';
|
|
14
|
+
const FORGE_LOOP_PATH = path.join(STATE_DIR, 'forge-loop.json');
|
|
15
|
+
const SOFT_STALE_MS = 24 * 60 * 60 * 1000;
|
|
16
|
+
const HARD_STALE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
17
|
+
const MAX_INJECT_BYTES = 1024;
|
|
18
|
+
const MAX_TASK_CHARS = 240;
|
|
19
|
+
const MAX_FINDING_CHARS = 240;
|
|
20
|
+
const MAX_PENDING = 5;
|
|
21
|
+
export function readForgeLoopState(filePath = FORGE_LOOP_PATH) {
|
|
22
|
+
try {
|
|
23
|
+
if (!fs.existsSync(filePath))
|
|
24
|
+
return null;
|
|
25
|
+
const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
26
|
+
if (typeof raw !== 'object' || raw === null)
|
|
27
|
+
return null;
|
|
28
|
+
return raw;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function ageMs(state, now = Date.now()) {
|
|
35
|
+
const ts = state.completedAt ?? state.startedAt;
|
|
36
|
+
if (!ts)
|
|
37
|
+
return Number.POSITIVE_INFINITY;
|
|
38
|
+
const t = new Date(ts).getTime();
|
|
39
|
+
if (Number.isNaN(t))
|
|
40
|
+
return Number.POSITIVE_INFINITY;
|
|
41
|
+
return now - t;
|
|
42
|
+
}
|
|
43
|
+
function clipBlock(block) {
|
|
44
|
+
if (block.length <= MAX_INJECT_BYTES)
|
|
45
|
+
return block;
|
|
46
|
+
return `${block.slice(0, MAX_INJECT_BYTES - 3)}...`;
|
|
47
|
+
}
|
|
48
|
+
function escXml(s) {
|
|
49
|
+
return s.replace(/[<>&"]/g, c => ({ '<': '<', '>': '>', '&': '&', '"': '"' })[c] ?? c);
|
|
50
|
+
}
|
|
51
|
+
/** SessionStart 용 — 완료된 forge-loop 의 findings 또는 진행 중 stories 요약. */
|
|
52
|
+
export function renderForgeLoopForSession(state, now = Date.now()) {
|
|
53
|
+
if (!state)
|
|
54
|
+
return null;
|
|
55
|
+
const age = ageMs(state, now);
|
|
56
|
+
if (age > HARD_STALE_MS)
|
|
57
|
+
return null;
|
|
58
|
+
const stale = age > SOFT_STALE_MS;
|
|
59
|
+
const lines = [];
|
|
60
|
+
const task = String(state.task ?? '').trim();
|
|
61
|
+
if (task)
|
|
62
|
+
lines.push(`Task: ${escXml(task.slice(0, MAX_TASK_CHARS))}`);
|
|
63
|
+
if (state.active && Array.isArray(state.stories)) {
|
|
64
|
+
const total = state.stories.length;
|
|
65
|
+
const done = state.stories.filter(s => s?.passes).length;
|
|
66
|
+
lines.push(`Status: in-progress ${done}/${total}${stale ? ' (stale)' : ''}`);
|
|
67
|
+
const pending = state.stories
|
|
68
|
+
.filter(s => !s?.passes)
|
|
69
|
+
.slice(0, MAX_PENDING)
|
|
70
|
+
.map(s => `- ${escXml(String(s.id))}: ${escXml(String(s.title))}`);
|
|
71
|
+
if (pending.length) {
|
|
72
|
+
lines.push('Pending:');
|
|
73
|
+
lines.push(...pending);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else if (state.findings && typeof state.findings === 'object') {
|
|
77
|
+
lines.push(`Status: completed${stale ? ' (stale)' : ''}`);
|
|
78
|
+
for (const [id, val] of Object.entries(state.findings)) {
|
|
79
|
+
const text = String(val ?? '').slice(0, MAX_FINDING_CHARS);
|
|
80
|
+
if (text)
|
|
81
|
+
lines.push(`- ${escXml(id)}: ${escXml(text)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (lines.length === 0)
|
|
88
|
+
return null;
|
|
89
|
+
const tag = stale ? '<forge-loop-state stale="true">' : '<forge-loop-state>';
|
|
90
|
+
const body = lines.join('\n');
|
|
91
|
+
return clipBlock(`${tag}\n${body}\n</forge-loop-state>`);
|
|
92
|
+
}
|
|
93
|
+
/** UserPromptSubmit 용 — active=true 시에만 짧은 진행 상황 1~2줄. */
|
|
94
|
+
export function renderForgeLoopForPrompt(state, now = Date.now()) {
|
|
95
|
+
if (!state || !state.active || !Array.isArray(state.stories))
|
|
96
|
+
return null;
|
|
97
|
+
const age = ageMs(state, now);
|
|
98
|
+
if (age > HARD_STALE_MS)
|
|
99
|
+
return null;
|
|
100
|
+
const total = state.stories.length;
|
|
101
|
+
const done = state.stories.filter(s => s?.passes).length;
|
|
102
|
+
const next = state.stories.find(s => !s?.passes);
|
|
103
|
+
if (!next)
|
|
104
|
+
return null;
|
|
105
|
+
const stale = age > SOFT_STALE_MS;
|
|
106
|
+
const tag = stale ? '<forge-loop-active stale="true">' : '<forge-loop-active>';
|
|
107
|
+
const body = `Progress: ${done}/${total} | next: ${escXml(String(next.id))} ${escXml(String(next.title))}`;
|
|
108
|
+
return clipBlock(`${tag}\n${body}\n</forge-loop-active>`);
|
|
109
|
+
}
|
|
110
|
+
/** 테스트 노출용 상수 — 회귀 시 임계값 변경 즉시 감지. */
|
|
111
|
+
export const FORGE_LOOP_LIMITS = {
|
|
112
|
+
SOFT_STALE_MS,
|
|
113
|
+
HARD_STALE_MS,
|
|
114
|
+
MAX_INJECT_BYTES,
|
|
115
|
+
MAX_PENDING,
|
|
116
|
+
};
|
|
@@ -34,6 +34,24 @@ export declare function approveWithWarning(warning: string): string;
|
|
|
34
34
|
export declare function deny(reason: string): string;
|
|
35
35
|
/** 사용자 확인 요청 (PreToolUse 전용) */
|
|
36
36
|
export declare function ask(reason: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* P3' enforcement helper (2026-04-27)
|
|
39
|
+
*
|
|
40
|
+
* ALLOW-LIST 에 있는 hook 만 진짜 deny — 아닌 경우 approve + 관찰 신호로 강등.
|
|
41
|
+
*
|
|
42
|
+
* 사용:
|
|
43
|
+
* ```ts
|
|
44
|
+
* import { canBlock } from './blocking-allowlist.js';
|
|
45
|
+
* if (someCondition) {
|
|
46
|
+
* console.log(blockOrObserve(hookName, 'reason', logCallback));
|
|
47
|
+
* return;
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* 점진 마이그레이션 — 본 helper 를 새로 사용하는 hook 은 ALLOW-LIST 외라면
|
|
52
|
+
* 자동 관찰 모드로 작동. 기존 hook 들은 별도 PR 에서 마이그레이션.
|
|
53
|
+
*/
|
|
54
|
+
export declare function denyOrObserve(hookName: string, reason: string, observer?: (msg: string) => void): string;
|
|
37
55
|
/**
|
|
38
56
|
* Stop hook only — block the agent from stopping and feed a self-check
|
|
39
57
|
* question back to Claude so the current session resumes with new guidance.
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import * as fs from 'node:fs';
|
|
17
17
|
import * as path from 'node:path';
|
|
18
18
|
import { STATE_DIR } from '../../core/paths.js';
|
|
19
|
+
import { canBlock } from './blocking-allowlist.js';
|
|
19
20
|
/** 통과 응답 (컨텍스트 없음, 모든 이벤트 공통) */
|
|
20
21
|
export function approve() {
|
|
21
22
|
return JSON.stringify({ continue: true });
|
|
@@ -65,6 +66,36 @@ export function ask(reason) {
|
|
|
65
66
|
},
|
|
66
67
|
});
|
|
67
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* P3' enforcement helper (2026-04-27)
|
|
71
|
+
*
|
|
72
|
+
* ALLOW-LIST 에 있는 hook 만 진짜 deny — 아닌 경우 approve + 관찰 신호로 강등.
|
|
73
|
+
*
|
|
74
|
+
* 사용:
|
|
75
|
+
* ```ts
|
|
76
|
+
* import { canBlock } from './blocking-allowlist.js';
|
|
77
|
+
* if (someCondition) {
|
|
78
|
+
* console.log(blockOrObserve(hookName, 'reason', logCallback));
|
|
79
|
+
* return;
|
|
80
|
+
* }
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* 점진 마이그레이션 — 본 helper 를 새로 사용하는 hook 은 ALLOW-LIST 외라면
|
|
84
|
+
* 자동 관찰 모드로 작동. 기존 hook 들은 별도 PR 에서 마이그레이션.
|
|
85
|
+
*/
|
|
86
|
+
export function denyOrObserve(hookName, reason, observer) {
|
|
87
|
+
if (canBlock(hookName)) {
|
|
88
|
+
return deny(reason);
|
|
89
|
+
}
|
|
90
|
+
// ALLOW-LIST 외 — log only, approve
|
|
91
|
+
if (observer) {
|
|
92
|
+
try {
|
|
93
|
+
observer(`[${hookName}] would-deny (observe-only): ${reason}`);
|
|
94
|
+
}
|
|
95
|
+
catch { /* fail-open */ }
|
|
96
|
+
}
|
|
97
|
+
return approve();
|
|
98
|
+
}
|
|
68
99
|
/**
|
|
69
100
|
* Stop hook only — block the agent from stopping and feed a self-check
|
|
70
101
|
* question back to Claude so the current session resumes with new guidance.
|
|
@@ -181,7 +181,7 @@ function collectSkills() {
|
|
|
181
181
|
const skills = [];
|
|
182
182
|
const seen = new Map(); // name → source dir
|
|
183
183
|
// 패키지 내장 스킬 경로 (dist/../skills/)
|
|
184
|
-
const pkgSkillsDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'commands');
|
|
184
|
+
const pkgSkillsDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'assets', 'claude', 'commands');
|
|
185
185
|
// 프로젝트 .forgen > 프로젝트 .compound > 개인 > 글로벌 > 패키지 내장
|
|
186
186
|
const dirs = [
|
|
187
187
|
path.join(process.cwd(), '.forgen', 'skills'),
|
package/dist/hooks/stop-guard.js
CHANGED
|
@@ -26,6 +26,7 @@ import { approve, approveWithWarning, blockStop, failOpenWithTracking } from './
|
|
|
26
26
|
import { takeLastExtractionNotice } from '../core/extraction-notice.js';
|
|
27
27
|
import { checkConclusionVerificationRatio } from '../checks/conclusion-verification-ratio.js';
|
|
28
28
|
import { checkSelfScoreInflation } from '../checks/self-score-deflation.js';
|
|
29
|
+
import { checkFactVsAgreement } from '../checks/fact-vs-agreement.js';
|
|
29
30
|
import { STATE_DIR } from '../core/paths.js';
|
|
30
31
|
import { sanitizeId } from './shared/sanitize-id.js';
|
|
31
32
|
import { detectRecallReferences } from '../core/recall-reference-detector.js';
|
|
@@ -533,6 +534,20 @@ export async function main() {
|
|
|
533
534
|
console.log(blockStop(reasonText, 'rule:TEST-3 — conclusion/verification ratio'));
|
|
534
535
|
return;
|
|
535
536
|
}
|
|
537
|
+
// TEST-1: 사실 vs 합의 — fact assertion 키워드가 있으나 측정 도구 호출 0건.
|
|
538
|
+
// 원 design intent (per fact-vs-agreement.ts): alert level only — block 은 TEST-2/3.
|
|
539
|
+
// 여기서는 measurement 신호를 violations.jsonl 에 'alert' kind 로 기록만 (block 안 함).
|
|
540
|
+
// wiring gap 발견 (forgen-eval introspect) → 측정 가능하게 wired up.
|
|
541
|
+
const fva = checkFactVsAgreement({ text: lastMessage, recentTools, minMeasurements: 1 });
|
|
542
|
+
if (fva.alert) {
|
|
543
|
+
recordViolation({
|
|
544
|
+
rule_id: 'builtin:fact-vs-agreement',
|
|
545
|
+
session_id: sessionId,
|
|
546
|
+
source: 'stop-guard',
|
|
547
|
+
kind: 'correction', // alert-level signal (not block) per fact-vs-agreement.ts design
|
|
548
|
+
message_preview: lastMessage.slice(0, 120),
|
|
549
|
+
});
|
|
550
|
+
}
|
|
536
551
|
}
|
|
537
552
|
const rules = loadStopRules();
|
|
538
553
|
if (rules.length === 0) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude HostCapabilities — Multi-Host Core Design §9.0
|
|
3
|
+
*
|
|
4
|
+
* Claude 는 reference host. 모든 TrustLayerIntent 가 supported (identity binding).
|
|
5
|
+
* 본 선언은 *spec 정의 그 자체* 의 코드 표현 — 변경 시 spec §9.0 도 같이 갱신해야 한다.
|
|
6
|
+
*/
|
|
7
|
+
import type { HostCapabilities } from '../core/trust-layer-intent.js';
|
|
8
|
+
export declare const claudeCapabilities: HostCapabilities;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude HostCapabilities — Multi-Host Core Design §9.0
|
|
3
|
+
*
|
|
4
|
+
* Claude 는 reference host. 모든 TrustLayerIntent 가 supported (identity binding).
|
|
5
|
+
* 본 선언은 *spec 정의 그 자체* 의 코드 표현 — 변경 시 spec §9.0 도 같이 갱신해야 한다.
|
|
6
|
+
*/
|
|
7
|
+
export const claudeCapabilities = {
|
|
8
|
+
hostId: 'claude',
|
|
9
|
+
intents: {
|
|
10
|
+
'block-completion': {
|
|
11
|
+
status: 'supported',
|
|
12
|
+
expression: 'Stop hook + `decision:"block"` + `reason`',
|
|
13
|
+
source: 'forgen v0.4.0 stop-guard, src/hooks/stop-guard.ts',
|
|
14
|
+
},
|
|
15
|
+
'block-tool-use': {
|
|
16
|
+
status: 'supported',
|
|
17
|
+
expression: 'PreToolUse + `hookSpecificOutput.permissionDecision:"deny"` + `permissionDecisionReason`',
|
|
18
|
+
source: 'forgen v0.4.0 pre-tool-use, src/hooks/pre-tool-use.ts',
|
|
19
|
+
},
|
|
20
|
+
'inject-context': {
|
|
21
|
+
status: 'supported',
|
|
22
|
+
expression: 'SessionStart/UserPromptSubmit + `hookSpecificOutput.additionalContext`',
|
|
23
|
+
source: 'forgen v0.4.2 M1, src/hooks/session-recovery.ts + forge-loop-progress.ts',
|
|
24
|
+
},
|
|
25
|
+
'observe-only': {
|
|
26
|
+
status: 'supported',
|
|
27
|
+
expression: 'non-allowlist hook approve + observer log (denyOrObserve)',
|
|
28
|
+
source: 'forgen v0.4.2 P3\', src/hooks/shared/blocking-allowlist.ts + hook-response.ts',
|
|
29
|
+
},
|
|
30
|
+
'secret-filter': {
|
|
31
|
+
status: 'supported',
|
|
32
|
+
expression: 'PreToolUse 가드 + (선택) PostToolUse 차단/redact',
|
|
33
|
+
source: 'forgen v0.4.0 secret-filter, src/hooks/secret-filter.ts',
|
|
34
|
+
},
|
|
35
|
+
'forge-loop-state-inject': {
|
|
36
|
+
status: 'supported',
|
|
37
|
+
expression: 'SessionStart/UserPromptSubmit + `<forge-loop-state>` ≤1KB additionalContext',
|
|
38
|
+
source: 'forgen v0.4.2 M1, src/hooks/shared/forge-loop-state.ts',
|
|
39
|
+
},
|
|
40
|
+
'self-evidence-record': {
|
|
41
|
+
status: 'supported',
|
|
42
|
+
expression: 'hook 결과 → ~/.forgen/state/*.json (host 무관)',
|
|
43
|
+
source: 'forgen v0.4.2, ~/.forgen/state/e2e-result.json 외',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex HostCapabilities — Multi-Host Core Design §9.0 + §18 (source-level verified)
|
|
3
|
+
*
|
|
4
|
+
* Codex 는 1원칙 (Claude reference) 의 등가 확장 host. schema-level 에서 7/7 supported.
|
|
5
|
+
* 단, secret-filter 는 PostToolUse `hookSpecificOutput.updatedMCPToolOutput` 이 MCP tool 한정이므로
|
|
6
|
+
* partial. 일반 shell/edit tool 의 결과 redact 는 미보장 — PreToolUse 가드 유지로 mitigation.
|
|
7
|
+
*
|
|
8
|
+
* source-of-truth: codex-rs/hooks/schema/generated/* (Apache-2.0). spec §17/§18 이 박제한 검증 결과.
|
|
9
|
+
*/
|
|
10
|
+
import type { HostCapabilities } from '../core/trust-layer-intent.js';
|
|
11
|
+
export declare const codexCapabilities: HostCapabilities;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex HostCapabilities — Multi-Host Core Design §9.0 + §18 (source-level verified)
|
|
3
|
+
*
|
|
4
|
+
* Codex 는 1원칙 (Claude reference) 의 등가 확장 host. schema-level 에서 7/7 supported.
|
|
5
|
+
* 단, secret-filter 는 PostToolUse `hookSpecificOutput.updatedMCPToolOutput` 이 MCP tool 한정이므로
|
|
6
|
+
* partial. 일반 shell/edit tool 의 결과 redact 는 미보장 — PreToolUse 가드 유지로 mitigation.
|
|
7
|
+
*
|
|
8
|
+
* source-of-truth: codex-rs/hooks/schema/generated/* (Apache-2.0). spec §17/§18 이 박제한 검증 결과.
|
|
9
|
+
*/
|
|
10
|
+
export const codexCapabilities = {
|
|
11
|
+
hostId: 'codex',
|
|
12
|
+
intents: {
|
|
13
|
+
'block-completion': {
|
|
14
|
+
status: 'supported',
|
|
15
|
+
expression: 'Stop + `decision:"block"` + `reason` (Codex 가 reason 을 다음 turn prompt 로 자동 주입)',
|
|
16
|
+
source: 'codex-rs/hooks/schema/generated/stop.command.output.schema.json — description 에 "Claude requires `reason` when `decision` is `block`" 명시',
|
|
17
|
+
},
|
|
18
|
+
'block-tool-use': {
|
|
19
|
+
status: 'supported',
|
|
20
|
+
expression: 'PreToolUse + `hookSpecificOutput.permissionDecision:"deny"` + `permissionDecisionReason`',
|
|
21
|
+
source: 'codex-rs/hooks/schema/generated/pre-tool-use.command.output.schema.json — PreToolUsePermissionDecisionWire enum ["allow","deny","ask"]',
|
|
22
|
+
},
|
|
23
|
+
'inject-context': {
|
|
24
|
+
status: 'supported',
|
|
25
|
+
expression: 'SessionStart/UserPromptSubmit + `hookSpecificOutput.additionalContext`',
|
|
26
|
+
source: 'codex-rs/hooks/schema/generated/{session-start,user-prompt-submit}.command.output.schema.json — additionalContext: string',
|
|
27
|
+
},
|
|
28
|
+
'observe-only': {
|
|
29
|
+
status: 'supported',
|
|
30
|
+
expression: 'non-allowlist hook approve + observer log (denyOrObserve 그대로)',
|
|
31
|
+
source: 'forgen denyOrObserve 가 stdout JSON 만 다루므로 host 무관 — spec §17.2 확인',
|
|
32
|
+
},
|
|
33
|
+
'secret-filter': {
|
|
34
|
+
status: 'partial',
|
|
35
|
+
expression: 'MCP tool 한정: PostToolUse + `hookSpecificOutput.updatedMCPToolOutput`. 일반 shell/edit tool 결과 redact 계약 부재.',
|
|
36
|
+
mitigation: '1차는 PreToolUse 단계의 secret-filter 가드 유지 (Claude 와 동일 경로). 일반 tool 결과 redact 는 향후 PostToolUse 도입 시 MCP tool 에 한해 강화.',
|
|
37
|
+
source: 'codex-rs/hooks/schema/generated/post-tool-use.command.output.schema.json — updatedMCPToolOutput 만 정의',
|
|
38
|
+
},
|
|
39
|
+
'forge-loop-state-inject': {
|
|
40
|
+
status: 'supported',
|
|
41
|
+
expression: 'SessionStart/UserPromptSubmit + `<forge-loop-state>` ≤1KB additionalContext',
|
|
42
|
+
source: 'spec §9.0 row 6 — schema 가 Claude 와 동치하므로 1KB cap 정책 그대로 적용',
|
|
43
|
+
},
|
|
44
|
+
'self-evidence-record': {
|
|
45
|
+
status: 'supported',
|
|
46
|
+
expression: 'hook 결과 → ~/.forgen/state/*.json (host 무관). evidence 에 host:"codex" 태그 추가만 필요.',
|
|
47
|
+
source: 'spec §4.2 host-tagged evidence',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host Capabilities Registry — Multi-Host Core Design §10 우선순위 1
|
|
3
|
+
*
|
|
4
|
+
* 등록된 모든 host 의 HostCapabilities 를 모듈 로드 시점에 검증한다.
|
|
5
|
+
* 새 TrustLayerIntent 추가 시 두 host 어댑터가 모두 선언을 추가하지 않으면 컴파일 fail.
|
|
6
|
+
* (TypeScript `Record<TrustLayerIntent, _>` 타입 + 이 모듈의 runtime assert 이중 가드.)
|
|
7
|
+
*/
|
|
8
|
+
import { type HostCapabilities, type HostId, type TrustLayerIntent } from '../core/trust-layer-intent.js';
|
|
9
|
+
export declare function getHostCapabilities(host: HostId): HostCapabilities;
|
|
10
|
+
export declare function listRegisteredHosts(): readonly HostId[];
|
|
11
|
+
export declare function intentSupported(host: HostId, intent: TrustLayerIntent): boolean;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host Capabilities Registry — Multi-Host Core Design §10 우선순위 1
|
|
3
|
+
*
|
|
4
|
+
* 등록된 모든 host 의 HostCapabilities 를 모듈 로드 시점에 검증한다.
|
|
5
|
+
* 새 TrustLayerIntent 추가 시 두 host 어댑터가 모두 선언을 추가하지 않으면 컴파일 fail.
|
|
6
|
+
* (TypeScript `Record<TrustLayerIntent, _>` 타입 + 이 모듈의 runtime assert 이중 가드.)
|
|
7
|
+
*/
|
|
8
|
+
import { assertCapabilitiesComplete, } from '../core/trust-layer-intent.js';
|
|
9
|
+
import { claudeCapabilities } from './capabilities-claude.js';
|
|
10
|
+
import { codexCapabilities } from './capabilities-codex.js';
|
|
11
|
+
const REGISTRY = new Map([
|
|
12
|
+
[claudeCapabilities.hostId, claudeCapabilities],
|
|
13
|
+
[codexCapabilities.hostId, codexCapabilities],
|
|
14
|
+
]);
|
|
15
|
+
// 모듈 로드 시점 자기 검증 — 하나라도 미선언이면 즉시 throw.
|
|
16
|
+
for (const caps of REGISTRY.values()) {
|
|
17
|
+
assertCapabilitiesComplete(caps);
|
|
18
|
+
}
|
|
19
|
+
export function getHostCapabilities(host) {
|
|
20
|
+
const caps = REGISTRY.get(host);
|
|
21
|
+
if (!caps)
|
|
22
|
+
throw new Error(`Unknown host: ${host}`);
|
|
23
|
+
return caps;
|
|
24
|
+
}
|
|
25
|
+
export function listRegisteredHosts() {
|
|
26
|
+
return Array.from(REGISTRY.keys());
|
|
27
|
+
}
|
|
28
|
+
export function intentSupported(host, intent) {
|
|
29
|
+
return getHostCapabilities(host).intents[intent].status === 'supported';
|
|
30
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Codex 훅 어댑터
|
|
3
|
+
* Codex 훅 어댑터 — Multi-Host Core Design §10 우선순위 2 (승격)
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* 본 binary 는 codex 런타임에서 실행되는 훅 스크립트 출력을 Claude Hook schema 로
|
|
6
|
+
* 사영(projection)한다. 사영 로직은 정식 계약 `ProjectToClaudeEvent` (src/host/projection.ts)
|
|
7
|
+
* 에서 제공하며, 본 파일은 그 계약의 *binary 진입점* 역할만 수행한다.
|
|
8
|
+
*
|
|
9
|
+
* - 입력: 사용자 hook 스크립트(stdin JSON, argv 의 첫 인자가 delegate path)
|
|
10
|
+
* - 출력: Claude HookEventOutput 동치 JSON (stdout 1줄)
|
|
11
|
+
* - 실패 정책: parse/실행 실패 → fail-open (`{ continue: true }`)
|
|
9
12
|
*/
|
|
10
13
|
export {};
|
|
@@ -1,49 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Codex 훅 어댑터
|
|
3
|
+
* Codex 훅 어댑터 — Multi-Host Core Design §10 우선순위 2 (승격)
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* 본 binary 는 codex 런타임에서 실행되는 훅 스크립트 출력을 Claude Hook schema 로
|
|
6
|
+
* 사영(projection)한다. 사영 로직은 정식 계약 `ProjectToClaudeEvent` (src/host/projection.ts)
|
|
7
|
+
* 에서 제공하며, 본 파일은 그 계약의 *binary 진입점* 역할만 수행한다.
|
|
8
|
+
*
|
|
9
|
+
* - 입력: 사용자 hook 스크립트(stdin JSON, argv 의 첫 인자가 delegate path)
|
|
10
|
+
* - 출력: Claude HookEventOutput 동치 JSON (stdout 1줄)
|
|
11
|
+
* - 실패 정책: parse/실행 실패 → fail-open (`{ continue: true }`)
|
|
9
12
|
*/
|
|
10
13
|
import { spawnSync } from 'node:child_process';
|
|
11
|
-
|
|
12
|
-
if (typeof raw === 'boolean') {
|
|
13
|
-
return { continueFlag: raw };
|
|
14
|
-
}
|
|
15
|
-
if (typeof raw === 'string') {
|
|
16
|
-
const normalized = raw.toLowerCase();
|
|
17
|
-
if (normalized === 'continue')
|
|
18
|
-
return { continueFlag: true };
|
|
19
|
-
if (normalized === 'stop' || normalized === 'deny' || normalized === 'reject' || normalized === 'block') {
|
|
20
|
-
return { continueFlag: false, permissionDecision: normalized };
|
|
21
|
-
}
|
|
22
|
-
return { continueFlag: true };
|
|
23
|
-
}
|
|
24
|
-
if (typeof raw !== 'object' || raw === null)
|
|
25
|
-
return { continueFlag: true };
|
|
26
|
-
const value = raw.decision;
|
|
27
|
-
if (typeof value === 'string') {
|
|
28
|
-
const normalized = value.toLowerCase();
|
|
29
|
-
if (normalized === 'deny' || normalized === 'reject' || normalized === 'block') {
|
|
30
|
-
return { continueFlag: false, permissionDecision: normalized };
|
|
31
|
-
}
|
|
32
|
-
if (normalized === 'ask' || normalized === 'prompt' || normalized === 'confirm') {
|
|
33
|
-
return { continueFlag: true, permissionDecision: normalized };
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
if (typeof raw.approved === 'boolean') {
|
|
37
|
-
const approved = raw.approved;
|
|
38
|
-
return approved
|
|
39
|
-
? { continueFlag: true, permissionDecision: raw.decision || 'approve' }
|
|
40
|
-
: { continueFlag: false, permissionDecision: 'deny' };
|
|
41
|
-
}
|
|
42
|
-
if (typeof raw.continue === 'boolean') {
|
|
43
|
-
return { continueFlag: raw.continue };
|
|
44
|
-
}
|
|
45
|
-
return { continueFlag: true };
|
|
46
|
-
}
|
|
14
|
+
import { projectCodexToClaude } from './projection.js';
|
|
47
15
|
function lastJSONObjectFromText(raw) {
|
|
48
16
|
const lines = raw.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
49
17
|
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
@@ -61,46 +29,6 @@ function lastJSONObjectFromText(raw) {
|
|
|
61
29
|
return null;
|
|
62
30
|
}
|
|
63
31
|
}
|
|
64
|
-
function normalizeOutput(raw, input) {
|
|
65
|
-
const result = { continue: true };
|
|
66
|
-
const decision = parseDecision(raw);
|
|
67
|
-
result.continue = decision.continueFlag;
|
|
68
|
-
if (typeof raw === 'object' && raw !== null) {
|
|
69
|
-
const payload = raw;
|
|
70
|
-
if (typeof payload.continue === 'boolean')
|
|
71
|
-
result.continue = payload.continue;
|
|
72
|
-
if (typeof payload.systemMessage === 'string')
|
|
73
|
-
result.systemMessage = payload.systemMessage;
|
|
74
|
-
if (typeof payload.suppressOutput === 'boolean')
|
|
75
|
-
result.suppressOutput = payload.suppressOutput;
|
|
76
|
-
if (typeof payload.hookSpecificOutput === 'object' && payload.hookSpecificOutput !== null) {
|
|
77
|
-
result.hookSpecificOutput = { ...payload.hookSpecificOutput };
|
|
78
|
-
}
|
|
79
|
-
if (typeof payload.decision === 'string') {
|
|
80
|
-
result.hookSpecificOutput = {
|
|
81
|
-
...(result.hookSpecificOutput ?? {}),
|
|
82
|
-
permissionDecision: payload.decision,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
const eventName = result.hookSpecificOutput?.hookEventName ?? input.hookEventName ?? input.event;
|
|
87
|
-
if (eventName) {
|
|
88
|
-
result.hookSpecificOutput = {
|
|
89
|
-
hookEventName: eventName,
|
|
90
|
-
...(result.hookSpecificOutput ?? {}),
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
if (!result.continue && !result.hookSpecificOutput?.permissionDecision) {
|
|
94
|
-
if (decision.permissionDecision)
|
|
95
|
-
result.hookSpecificOutput = {
|
|
96
|
-
...(result.hookSpecificOutput ?? {}),
|
|
97
|
-
permissionDecision: decision.permissionDecision,
|
|
98
|
-
};
|
|
99
|
-
else
|
|
100
|
-
result.hookSpecificOutput = { ...(result.hookSpecificOutput ?? {}), permissionDecision: 'deny' };
|
|
101
|
-
}
|
|
102
|
-
return result;
|
|
103
|
-
}
|
|
104
32
|
async function main() {
|
|
105
33
|
const [delegatePath, ...restArgs] = process.argv.slice(2);
|
|
106
34
|
if (!delegatePath) {
|
|
@@ -142,7 +70,7 @@ async function main() {
|
|
|
142
70
|
console.log(JSON.stringify({ continue: true }));
|
|
143
71
|
return;
|
|
144
72
|
}
|
|
145
|
-
const output =
|
|
73
|
+
const output = projectCodexToClaude(parsed, input);
|
|
146
74
|
console.log(JSON.stringify(output));
|
|
147
75
|
}
|
|
148
76
|
catch {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex exec --json 출력 파서 — feat/codex-support Phase 2 (P2-1)
|
|
3
|
+
*
|
|
4
|
+
* codex exec --json 의 stdout 은 JSONL — 한 줄에 하나씩 이벤트.
|
|
5
|
+
* 본 파서는 agent_message 만 추출하여 문자열로 반환.
|
|
6
|
+
*
|
|
7
|
+
* 출력 형식 (실측 2026-04-27, Codex 0.125.0):
|
|
8
|
+
* {"type":"thread.started","thread_id":"..."}
|
|
9
|
+
* {"type":"turn.started"}
|
|
10
|
+
* {"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"..."}}
|
|
11
|
+
* {"type":"turn.completed","usage":{...}}
|
|
12
|
+
*
|
|
13
|
+
* spec §10 P2-1 산출물 — Phase 2 의 compound-extractor 가 host-aware 분기 시 사용.
|
|
14
|
+
*/
|
|
15
|
+
export interface CodexUsage {
|
|
16
|
+
input_tokens?: number;
|
|
17
|
+
cached_input_tokens?: number;
|
|
18
|
+
output_tokens?: number;
|
|
19
|
+
reasoning_output_tokens?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface CodexExecResult {
|
|
22
|
+
/** 모든 agent_message text 를 join. 보통 1개. */
|
|
23
|
+
readonly message: string;
|
|
24
|
+
/** 모든 agent_message segment (디버깅/multi-turn 용). */
|
|
25
|
+
readonly segments: ReadonlyArray<string>;
|
|
26
|
+
/** turn.completed 의 usage (없으면 null). */
|
|
27
|
+
readonly usage: CodexUsage | null;
|
|
28
|
+
/** thread.started 의 thread_id. */
|
|
29
|
+
readonly threadId: string | null;
|
|
30
|
+
/** parse 실패한 line 수 (의미 있는 신호 — 0 이 아니면 형식 변경 신호). */
|
|
31
|
+
readonly parseFailures: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* codex exec --json 의 stdout 을 받아 agent message + 메타 추출.
|
|
35
|
+
* stderr 의 hook 발화 noise 는 *별도* — 본 함수는 stdout 만 처리.
|
|
36
|
+
*
|
|
37
|
+
* fail-open: parse 실패 line 은 무시하되 카운터로 보고.
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseCodexJsonlOutput(stdout: string): CodexExecResult;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex exec --json 출력 파서 — feat/codex-support Phase 2 (P2-1)
|
|
3
|
+
*
|
|
4
|
+
* codex exec --json 의 stdout 은 JSONL — 한 줄에 하나씩 이벤트.
|
|
5
|
+
* 본 파서는 agent_message 만 추출하여 문자열로 반환.
|
|
6
|
+
*
|
|
7
|
+
* 출력 형식 (실측 2026-04-27, Codex 0.125.0):
|
|
8
|
+
* {"type":"thread.started","thread_id":"..."}
|
|
9
|
+
* {"type":"turn.started"}
|
|
10
|
+
* {"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"..."}}
|
|
11
|
+
* {"type":"turn.completed","usage":{...}}
|
|
12
|
+
*
|
|
13
|
+
* spec §10 P2-1 산출물 — Phase 2 의 compound-extractor 가 host-aware 분기 시 사용.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* codex exec --json 의 stdout 을 받아 agent message + 메타 추출.
|
|
17
|
+
* stderr 의 hook 발화 noise 는 *별도* — 본 함수는 stdout 만 처리.
|
|
18
|
+
*
|
|
19
|
+
* fail-open: parse 실패 line 은 무시하되 카운터로 보고.
|
|
20
|
+
*/
|
|
21
|
+
export function parseCodexJsonlOutput(stdout) {
|
|
22
|
+
const segments = [];
|
|
23
|
+
let usage = null;
|
|
24
|
+
let threadId = null;
|
|
25
|
+
let parseFailures = 0;
|
|
26
|
+
for (const line of stdout.split('\n')) {
|
|
27
|
+
const trimmed = line.trim();
|
|
28
|
+
if (!trimmed)
|
|
29
|
+
continue;
|
|
30
|
+
if (!trimmed.startsWith('{'))
|
|
31
|
+
continue; // ANSI / status line skip
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(trimmed);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
parseFailures += 1;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
41
|
+
continue;
|
|
42
|
+
const event = parsed;
|
|
43
|
+
const type = event.type;
|
|
44
|
+
if (type === 'thread.started') {
|
|
45
|
+
const tid = event.thread_id;
|
|
46
|
+
if (typeof tid === 'string')
|
|
47
|
+
threadId = tid;
|
|
48
|
+
}
|
|
49
|
+
else if (type === 'item.completed') {
|
|
50
|
+
const item = event.item;
|
|
51
|
+
if (item?.type === 'agent_message') {
|
|
52
|
+
// Phase 2 critic fix: text 가 string 아니면 schema drift 신호 → parseFailures 증가.
|
|
53
|
+
// (Codex 가 향후 array/object content 형식 도입 시 silent miss 방지)
|
|
54
|
+
if (typeof item.text === 'string') {
|
|
55
|
+
segments.push(item.text);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
parseFailures += 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (type === 'turn.completed') {
|
|
63
|
+
const u = event.usage;
|
|
64
|
+
if (u && typeof u === 'object')
|
|
65
|
+
usage = u;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
message: segments.join('\n'),
|
|
70
|
+
segments,
|
|
71
|
+
usage,
|
|
72
|
+
threadId,
|
|
73
|
+
parseFailures,
|
|
74
|
+
};
|
|
75
|
+
}
|