@wooojin/forgen 0.4.7 → 0.4.9
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 +1 -1
- package/CHANGELOG.md +40 -0
- package/assets/dev-guide/be/README.md +226 -0
- package/assets/dev-guide/be/adapters/build-agents-md.sh +63 -0
- package/assets/dev-guide/be/principles/common.md +433 -0
- package/assets/dev-guide/be/principles/go.md +469 -0
- package/assets/dev-guide/be/principles/node.md +388 -0
- package/assets/dev-guide/be/skills/go/be-build/SKILL.md +262 -0
- package/assets/dev-guide/be/skills/go/be-perf/SKILL.md +308 -0
- package/assets/dev-guide/be/skills/go/be-review/SKILL.md +119 -0
- package/assets/dev-guide/be/skills/go/be-security/SKILL.md +362 -0
- package/assets/dev-guide/be/skills/node/be-build/SKILL.md +239 -0
- package/assets/dev-guide/be/skills/node/be-perf/SKILL.md +272 -0
- package/assets/dev-guide/be/skills/node/be-review/SKILL.md +118 -0
- package/assets/dev-guide/be/skills/node/be-security/SKILL.md +355 -0
- package/assets/dev-guide/be/sources/12factor/INDEX.md +53 -0
- package/assets/dev-guide/be/sources/api-design/INDEX.md +56 -0
- package/assets/dev-guide/be/sources/ddia/INDEX.md +55 -0
- package/assets/dev-guide/be/sources/go-runtime/INDEX.md +62 -0
- package/assets/dev-guide/be/sources/node-runtime/INDEX.md +60 -0
- package/assets/dev-guide/be/sources/otel/INDEX.md +53 -0
- package/assets/dev-guide/be/sources/owasp-api/INDEX.md +52 -0
- package/assets/dev-guide/be/sources/postgres/INDEX.md +55 -0
- package/assets/dev-guide/be/sources/sre-book/INDEX.md +48 -0
- package/assets/dev-guide/fe/README.md +197 -0
- package/assets/dev-guide/fe/adapters/build-agents-md.sh +63 -0
- package/assets/dev-guide/fe/adapters/refresh.sh +68 -0
- package/assets/dev-guide/fe/principles/common.md +160 -0
- package/assets/dev-guide/fe/principles/react.md +183 -0
- package/assets/dev-guide/fe/principles/vue.md +196 -0
- package/assets/dev-guide/fe/skills/react/fe-build/SKILL.md +139 -0
- package/assets/dev-guide/fe/skills/react/fe-perf/SKILL.md +179 -0
- package/assets/dev-guide/fe/skills/react/fe-review/SKILL.md +141 -0
- package/assets/dev-guide/fe/skills/vue/fe-build/SKILL.md +148 -0
- package/assets/dev-guide/fe/skills/vue/fe-perf/SKILL.md +163 -0
- package/assets/dev-guide/fe/skills/vue/fe-review/SKILL.md +136 -0
- package/assets/dev-guide/fe/sources/a11y-dx/INDEX.md +41 -0
- package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-memory.md +150 -0
- package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-performance.md +99 -0
- package/assets/dev-guide/fe/sources/a11y-dx/lighthouse-audits.md +146 -0
- package/assets/dev-guide/fe/sources/a11y-dx/react-devtools-profiler.md +128 -0
- package/assets/dev-guide/fe/sources/a11y-dx/wcag22-new-criteria.md +174 -0
- package/assets/dev-guide/fe/sources/perf/01-core-web-vitals.md +58 -0
- package/assets/dev-guide/fe/sources/perf/02-inp.md +83 -0
- package/assets/dev-guide/fe/sources/perf/03-lcp-cls.md +130 -0
- package/assets/dev-guide/fe/sources/perf/04-speculation-rules.md +148 -0
- package/assets/dev-guide/fe/sources/perf/05-view-transitions.md +153 -0
- package/assets/dev-guide/fe/sources/perf/06-nextjs-caching.md +188 -0
- package/assets/dev-guide/fe/sources/perf/07-server-components.md +181 -0
- package/assets/dev-guide/fe/sources/perf/08-ppr.md +133 -0
- package/assets/dev-guide/fe/sources/perf/09-nextjs-image.md +200 -0
- package/assets/dev-guide/fe/sources/perf/10-optimize-lcp.md +201 -0
- package/assets/dev-guide/fe/sources/perf/INDEX.md +88 -0
- package/assets/dev-guide/fe/sources/react/INDEX.md +41 -0
- package/assets/dev-guide/fe/sources/react/keeping-components-pure.md +135 -0
- package/assets/dev-guide/fe/sources/react/no-effect-patterns.md +183 -0
- package/assets/dev-guide/fe/sources/react/react-compiler.md +182 -0
- package/assets/dev-guide/fe/sources/react/server-components.md +194 -0
- package/assets/dev-guide/fe/sources/react/server-functions.md +192 -0
- package/assets/dev-guide/fe/sources/react/suspense.md +218 -0
- package/assets/dev-guide/fe/sources/react/use-action-state.md +123 -0
- package/assets/dev-guide/fe/sources/react/use-form-status.md +158 -0
- package/assets/dev-guide/fe/sources/react/use-hook.md +153 -0
- package/assets/dev-guide/fe/sources/react/use-optimistic.md +194 -0
- package/assets/dev-guide/fe/sources/toss-ff/INDEX.md +58 -0
- package/assets/dev-guide/fe/sources/toss-ff/cohesion-code-directory.md +79 -0
- package/assets/dev-guide/fe/sources/toss-ff/cohesion-form-fields.md +110 -0
- package/assets/dev-guide/fe/sources/toss-ff/cohesion-magic-number.md +47 -0
- package/assets/dev-guide/fe/sources/toss-ff/coupling-item-edit-modal.md +124 -0
- package/assets/dev-guide/fe/sources/toss-ff/coupling-use-bottom-sheet.md +57 -0
- package/assets/dev-guide/fe/sources/toss-ff/coupling-use-page-state.md +71 -0
- package/assets/dev-guide/fe/sources/toss-ff/overview-4-principles.md +77 -0
- package/assets/dev-guide/fe/sources/toss-ff/predictability-hidden-logic.md +59 -0
- package/assets/dev-guide/fe/sources/toss-ff/predictability-http.md +77 -0
- package/assets/dev-guide/fe/sources/toss-ff/predictability-use-user.md +110 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-comparison-order.md +52 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-condition-name.md +64 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-login-start-page.md +183 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-magic-number.md +53 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-submit-button.md +73 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-ternary-operator.md +38 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-use-page-state.md +77 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-user-policy.md +98 -0
- package/assets/dev-guide/fe/sources/vue/INDEX.md +17 -0
- package/assets/dev-guide/fe/sources/vue/composition-api.md +251 -0
- package/assets/dev-guide/fe/sources/vue/nuxt-data-fetching.md +232 -0
- package/assets/dev-guide/fe/sources/vue/pinia-state-management.md +134 -0
- package/assets/dev-guide/fe/sources/vue/reactivity-pitfalls.md +261 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-a.md +117 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-b.md +231 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-c.md +86 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-d.md +72 -0
- package/dist/checks/self-score-deflation.js +6 -4
- package/dist/cli.js +47 -2
- package/dist/core/auto-compound-runner.js +6 -2
- package/dist/core/dashboard-cli.d.ts +12 -0
- package/dist/core/dashboard-cli.js +226 -0
- package/dist/core/dashboard.js +2 -2
- package/dist/core/dev-guide-injector.d.ts +26 -0
- package/dist/core/dev-guide-injector.js +137 -0
- package/dist/core/doctor.d.ts +10 -0
- package/dist/core/doctor.js +49 -8
- package/dist/core/harness.js +8 -2
- package/dist/core/init.js +53 -0
- package/dist/core/inspect-cli.js +4 -4
- package/dist/core/lifecycle-classifier.d.ts +23 -0
- package/dist/core/lifecycle-classifier.js +104 -0
- package/dist/core/migrate-evidence-host.js +1 -1
- package/dist/core/notify.js +7 -0
- package/dist/core/observability-backfill.d.ts +31 -0
- package/dist/core/observability-backfill.js +178 -0
- package/dist/core/observability-store.d.ts +58 -0
- package/dist/core/observability-store.js +195 -0
- package/dist/core/paths.d.ts +16 -2
- package/dist/core/paths.js +16 -2
- package/dist/core/session-store.d.ts +12 -1
- package/dist/core/session-store.js +77 -1
- package/dist/core/spawn.d.ts +17 -0
- package/dist/core/spawn.js +191 -8
- package/dist/core/statusline-cli.js +34 -1
- package/dist/core/v1-bootstrap.d.ts +7 -0
- package/dist/core/v1-bootstrap.js +28 -6
- package/dist/engine/compound-extractor.js +40 -1
- package/dist/engine/compound-loop.js +6 -0
- package/dist/engine/compound-retire.d.ts +20 -0
- package/dist/engine/compound-retire.js +85 -0
- package/dist/engine/learn-cli.js +2 -2
- package/dist/engine/lifecycle/bypass-detector.js +3 -2
- package/dist/engine/lifecycle/meta-reclassifier.js +1 -1
- package/dist/engine/lifecycle/signals.js +2 -2
- package/dist/engine/lifecycle/trigger-t1-correction.js +1 -1
- package/dist/engine/solution-candidate.js +1 -1
- package/dist/engine/solution-outcomes.js +1 -1
- package/dist/engine/solution-quarantine.js +1 -1
- package/dist/engine/solution-weakness.js +8 -2
- package/dist/forge/cli.js +1 -1
- package/dist/hooks/context-guard.js +25 -1
- package/dist/hooks/keyword-detector.js +1 -1
- package/dist/hooks/post-tool-use.js +48 -0
- package/dist/hooks/secret-filter.js +2 -2
- package/dist/hooks/shared/hook-response.js +1 -1
- package/dist/hooks/shared/hook-timing.js +3 -3
- package/dist/hooks/solution-injector.js +94 -1
- package/dist/hooks/stop-guard.js +3 -3
- package/dist/host/install-claude.d.ts +6 -2
- package/dist/host/install-claude.js +74 -2
- package/dist/host/install-codex.d.ts +4 -0
- package/dist/host/install-codex.js +72 -1
- package/dist/host/install-orchestrator.js +1 -0
- package/dist/mcp/tools.js +1 -1
- package/dist/preset/facet-catalog.js +2 -2
- package/dist/renderer/rule-renderer.js +7 -7
- package/dist/store/compound-usage-store.js +1 -1
- package/dist/store/implicit-feedback-store.js +2 -2
- package/dist/store/profile-store.d.ts +11 -0
- package/dist/store/profile-store.js +23 -0
- package/package.json +6 -6
- package/plugin.json +1 -1
- package/scripts/postinstall.js +134 -0
package/dist/core/paths.d.ts
CHANGED
|
@@ -60,7 +60,15 @@ export declare const OUTCOMES_DIR: string;
|
|
|
60
60
|
export declare const CANDIDATES_DIR: string;
|
|
61
61
|
/** ~/.forgen/lab/archived/ — rollback destination for evolved solutions. */
|
|
62
62
|
export declare const ARCHIVED_DIR: string;
|
|
63
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* ~/.forgen/sessions/ — legacy session log directory (transcript-like).
|
|
65
|
+
*
|
|
66
|
+
* session-logger.ts 가 prepareHarness step 11 에서 한 줄짜리
|
|
67
|
+
* `{date}_{uuid}.json` 메타 파일을 기록. v1 의 SessionEffectiveState
|
|
68
|
+
* 와는 다른 책임이라 별 디렉토리 (V1_SESSIONS_DIR) 와 공존:
|
|
69
|
+
* - SESSIONS_DIR (여기): 세션 발생 사실 + 시작/종료 시각 + cwd
|
|
70
|
+
* - V1_SESSIONS_DIR : profile + pack 합성 결과 (effective state)
|
|
71
|
+
*/
|
|
64
72
|
export declare const SESSIONS_DIR: string;
|
|
65
73
|
/** ~/.forgen/config.json — 글로벌 설정 */
|
|
66
74
|
export declare const GLOBAL_CONFIG: string;
|
|
@@ -72,7 +80,13 @@ export declare const LAB_DIR: string;
|
|
|
72
80
|
export declare const FORGE_PROFILE: string;
|
|
73
81
|
/** ~/.forgen/me/recommendations/ — Pack Recommendation */
|
|
74
82
|
export declare const V1_RECOMMENDATIONS_DIR: string;
|
|
75
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* ~/.forgen/state/sessions/ — v1 Session Effective State.
|
|
85
|
+
*
|
|
86
|
+
* session-state-store.ts 가 매 세션마다 profile + active rules + pack
|
|
87
|
+
* 합성 결과를 `{sessionId}.json` 으로 기록. SESSIONS_DIR (legacy session
|
|
88
|
+
* log) 와는 다른 책임 (정합화는 v0.4.8 A3 참조).
|
|
89
|
+
*/
|
|
76
90
|
export declare const V1_SESSIONS_DIR: string;
|
|
77
91
|
/** ~/.forgen/state/raw-logs/ — Raw Log */
|
|
78
92
|
export declare const V1_RAW_LOGS_DIR: string;
|
package/dist/core/paths.js
CHANGED
|
@@ -65,7 +65,15 @@ export const OUTCOMES_DIR = path.join(STATE_DIR, 'outcomes');
|
|
|
65
65
|
export const CANDIDATES_DIR = path.join(FORGEN_HOME, 'lab', 'candidates');
|
|
66
66
|
/** ~/.forgen/lab/archived/ — rollback destination for evolved solutions. */
|
|
67
67
|
export const ARCHIVED_DIR = path.join(FORGEN_HOME, 'lab', 'archived');
|
|
68
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* ~/.forgen/sessions/ — legacy session log directory (transcript-like).
|
|
70
|
+
*
|
|
71
|
+
* session-logger.ts 가 prepareHarness step 11 에서 한 줄짜리
|
|
72
|
+
* `{date}_{uuid}.json` 메타 파일을 기록. v1 의 SessionEffectiveState
|
|
73
|
+
* 와는 다른 책임이라 별 디렉토리 (V1_SESSIONS_DIR) 와 공존:
|
|
74
|
+
* - SESSIONS_DIR (여기): 세션 발생 사실 + 시작/종료 시각 + cwd
|
|
75
|
+
* - V1_SESSIONS_DIR : profile + pack 합성 결과 (effective state)
|
|
76
|
+
*/
|
|
69
77
|
export const SESSIONS_DIR = path.join(FORGEN_HOME, 'sessions');
|
|
70
78
|
/** ~/.forgen/config.json — 글로벌 설정 */
|
|
71
79
|
export const GLOBAL_CONFIG = path.join(FORGEN_HOME, 'config.json');
|
|
@@ -77,7 +85,13 @@ export const LAB_DIR = path.join(FORGEN_HOME, 'lab');
|
|
|
77
85
|
export const FORGE_PROFILE = path.join(ME_DIR, 'forge-profile.json');
|
|
78
86
|
/** ~/.forgen/me/recommendations/ — Pack Recommendation */
|
|
79
87
|
export const V1_RECOMMENDATIONS_DIR = path.join(ME_DIR, 'recommendations');
|
|
80
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* ~/.forgen/state/sessions/ — v1 Session Effective State.
|
|
90
|
+
*
|
|
91
|
+
* session-state-store.ts 가 매 세션마다 profile + active rules + pack
|
|
92
|
+
* 합성 결과를 `{sessionId}.json` 으로 기록. SESSIONS_DIR (legacy session
|
|
93
|
+
* log) 와는 다른 책임 (정합화는 v0.4.8 A3 참조).
|
|
94
|
+
*/
|
|
81
95
|
export const V1_SESSIONS_DIR = path.join(STATE_DIR, 'sessions');
|
|
82
96
|
/** ~/.forgen/state/raw-logs/ — Raw Log */
|
|
83
97
|
export const V1_RAW_LOGS_DIR = path.join(STATE_DIR, 'raw-logs');
|
|
@@ -6,7 +6,18 @@
|
|
|
6
6
|
* 외부 의존성 없음 — Node.js 22+ 내장 node:sqlite 사용.
|
|
7
7
|
*/
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* v0.4.8 (A1): Codex transcript JSONL 을 SQLite 에 인덱싱.
|
|
10
|
+
*
|
|
11
|
+
* Codex schema (Claude 와 다름):
|
|
12
|
+
* {type: 'response_item', payload: {type: 'message', role: 'user'|'assistant',
|
|
13
|
+
* content: [{type: 'input_text'|'output_text', text: '...'}]}}
|
|
14
|
+
*
|
|
15
|
+
* 결정 (v0.4.8 A1): Claude/Codex 통합 abstraction 대신 분기 함수 두 개로
|
|
16
|
+
* 처리. 미래에 host 가 추가될 때 통합 추상화로 리팩터.
|
|
17
|
+
*/
|
|
18
|
+
export declare function indexCodexSession(cwd: string, transcriptPath: string, sessionId: string): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Transcript JSONL을 SQLite에 인덱싱. (Claude schema)
|
|
10
21
|
*/
|
|
11
22
|
export declare function indexSession(cwd: string, transcriptPath: string, sessionId: string): Promise<void>;
|
|
12
23
|
/**
|
|
@@ -10,6 +10,7 @@ import * as fs from 'node:fs';
|
|
|
10
10
|
import * as path from 'node:path';
|
|
11
11
|
import { createLogger } from './logger.js';
|
|
12
12
|
import { FORGEN_HOME } from './paths.js';
|
|
13
|
+
import { ensureObservabilitySchema } from './observability-store.js';
|
|
13
14
|
const require = createRequire(import.meta.url);
|
|
14
15
|
// Suppress ExperimentalWarning for node:sqlite (Node.js 22+)
|
|
15
16
|
{
|
|
@@ -31,6 +32,7 @@ function openDb() {
|
|
|
31
32
|
// Node.js 22+ experimental node:sqlite
|
|
32
33
|
const { DatabaseSync } = require('node:sqlite');
|
|
33
34
|
const db = new DatabaseSync(DB_PATH);
|
|
35
|
+
db.exec(`PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA busy_timeout=1000;`);
|
|
34
36
|
db.exec(`
|
|
35
37
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
36
38
|
id TEXT PRIMARY KEY,
|
|
@@ -62,6 +64,8 @@ function openDb() {
|
|
|
62
64
|
log.debug('FTS5 미지원 — LIKE 폴백 사용', e);
|
|
63
65
|
fts5Available = false;
|
|
64
66
|
}
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
68
|
+
ensureObservabilitySchema(db);
|
|
65
69
|
return db;
|
|
66
70
|
}
|
|
67
71
|
catch (e) {
|
|
@@ -69,8 +73,80 @@ function openDb() {
|
|
|
69
73
|
return null;
|
|
70
74
|
}
|
|
71
75
|
}
|
|
76
|
+
/** Codex content array → flat string. content: [{type: 'input_text', text: ...}, ...] */
|
|
77
|
+
function extractCodexText(content) {
|
|
78
|
+
if (!Array.isArray(content))
|
|
79
|
+
return '';
|
|
80
|
+
const parts = [];
|
|
81
|
+
for (const item of content) {
|
|
82
|
+
if (item && typeof item === 'object' && 'text' in item && typeof item.text === 'string') {
|
|
83
|
+
parts.push(item.text);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return parts.join('\n').trim();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* v0.4.8 (A1): Codex transcript JSONL 을 SQLite 에 인덱싱.
|
|
90
|
+
*
|
|
91
|
+
* Codex schema (Claude 와 다름):
|
|
92
|
+
* {type: 'response_item', payload: {type: 'message', role: 'user'|'assistant',
|
|
93
|
+
* content: [{type: 'input_text'|'output_text', text: '...'}]}}
|
|
94
|
+
*
|
|
95
|
+
* 결정 (v0.4.8 A1): Claude/Codex 통합 abstraction 대신 분기 함수 두 개로
|
|
96
|
+
* 처리. 미래에 host 가 추가될 때 통합 추상화로 리팩터.
|
|
97
|
+
*/
|
|
98
|
+
export async function indexCodexSession(cwd, transcriptPath, sessionId) {
|
|
99
|
+
const db = openDb();
|
|
100
|
+
if (!db)
|
|
101
|
+
return;
|
|
102
|
+
try {
|
|
103
|
+
const existing = db.prepare('SELECT id FROM sessions WHERE id = ?').get(sessionId);
|
|
104
|
+
if (existing)
|
|
105
|
+
return;
|
|
106
|
+
const content = fs.readFileSync(transcriptPath, 'utf-8');
|
|
107
|
+
const lines = content.split('\n').filter(Boolean);
|
|
108
|
+
db.prepare('INSERT INTO sessions (id, cwd, started_at, message_count) VALUES (?, ?, ?, 0)').run(sessionId, cwd, new Date().toISOString());
|
|
109
|
+
let messageCount = 0;
|
|
110
|
+
const insertMsg = db.prepare('INSERT INTO messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)');
|
|
111
|
+
for (const line of lines) {
|
|
112
|
+
try {
|
|
113
|
+
const entry = JSON.parse(line);
|
|
114
|
+
if (entry.type !== 'response_item' || !entry.payload)
|
|
115
|
+
continue;
|
|
116
|
+
const role = entry.payload.role;
|
|
117
|
+
if (role !== 'user' && role !== 'assistant')
|
|
118
|
+
continue;
|
|
119
|
+
const text = extractCodexText(entry.payload.content);
|
|
120
|
+
if (!text)
|
|
121
|
+
continue;
|
|
122
|
+
const truncated = text.slice(0, 10000);
|
|
123
|
+
const ts = typeof entry.timestamp === 'string' ? entry.timestamp : '';
|
|
124
|
+
const result = insertMsg.run(sessionId, role, truncated, ts);
|
|
125
|
+
if (fts5Available) {
|
|
126
|
+
try {
|
|
127
|
+
db.prepare('INSERT INTO messages_fts(rowid, content) VALUES (?, ?)').run(result.lastInsertRowid, truncated);
|
|
128
|
+
}
|
|
129
|
+
catch { /* FTS sync failure */ }
|
|
130
|
+
}
|
|
131
|
+
messageCount++;
|
|
132
|
+
}
|
|
133
|
+
catch { /* skip malformed lines */ }
|
|
134
|
+
}
|
|
135
|
+
db.prepare('UPDATE sessions SET message_count = ? WHERE id = ?').run(messageCount, sessionId);
|
|
136
|
+
log.debug(`Codex 세션 인덱싱 완료: ${sessionId} (${messageCount} messages)`);
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
log.debug('Codex 세션 인덱싱 실패', e);
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
try {
|
|
143
|
+
db.close();
|
|
144
|
+
}
|
|
145
|
+
catch { /* ignore */ }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
72
148
|
/**
|
|
73
|
-
* Transcript JSONL을 SQLite에 인덱싱.
|
|
149
|
+
* Transcript JSONL을 SQLite에 인덱싱. (Claude schema)
|
|
74
150
|
*/
|
|
75
151
|
export async function indexSession(cwd, transcriptPath, sessionId) {
|
|
76
152
|
const db = openDb();
|
package/dist/core/spawn.d.ts
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import type { V1HarnessContext } from './harness.js';
|
|
2
2
|
import type { RuntimeHost } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Plan B-1: 세션 transcript 사후 스캔으로 rate-limit 감지.
|
|
5
|
+
*
|
|
6
|
+
* stdio='inherit' 라 Claude 출력을 직접 캡처할 수 없으므로,
|
|
7
|
+
* 세션 종료 후 transcript JSONL 의 마지막 N 라인을 읽어
|
|
8
|
+
* RATE_LIMIT_REGEX 매칭 여부를 확인한다.
|
|
9
|
+
*
|
|
10
|
+
* - transcript 없음 / 빈 파일 / parse 실패 → matched: false (fail-open)
|
|
11
|
+
* - 이미 pending-resume.json 존재 시 hook 이 먼저 잡은 것이므로 덮어쓰지 않음
|
|
12
|
+
*
|
|
13
|
+
* @param transcriptPath JSONL 파일 경로
|
|
14
|
+
* @param tailLines 검사할 마지막 라인 수 (기본 5)
|
|
15
|
+
*/
|
|
16
|
+
export declare function scanTranscriptForRateLimit(transcriptPath: string, tailLines?: number): Promise<{
|
|
17
|
+
matched: boolean;
|
|
18
|
+
resetAt: string | null;
|
|
19
|
+
}>;
|
|
3
20
|
/** Claude Code를 하네스 환경으로 실행. exit code를 반환. */
|
|
4
21
|
export declare function spawnClaude(args: string[], context: V1HarnessContext, runtime?: RuntimeHost): Promise<number>;
|
|
5
22
|
declare const MAX_RESUMES = 3;
|
package/dist/core/spawn.js
CHANGED
|
@@ -2,13 +2,16 @@ import { spawn, execFileSync } from 'node:child_process';
|
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import * as os from 'node:os';
|
|
5
|
+
import * as crypto from 'node:crypto';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import { buildEnv } from './config-injector.js';
|
|
7
8
|
import { loadGlobalConfig } from './global-config.js';
|
|
8
9
|
import { createLogger } from './logger.js';
|
|
9
|
-
import { STATE_DIR } from './paths.js';
|
|
10
|
+
import { STATE_DIR, ME_SOLUTIONS } from './paths.js';
|
|
10
11
|
import { getHostRuntime } from '../host/host-runtime.js';
|
|
11
12
|
import { sendNotification } from './notify.js';
|
|
13
|
+
import { querySurfacedWithin, emitSolutionEvent } from './observability-store.js';
|
|
14
|
+
import { parseSolutionV3 } from '../engine/solution-format.js';
|
|
12
15
|
const log = createLogger('spawn');
|
|
13
16
|
/** Phase 2: host-runtime 어댑터 위임. */
|
|
14
17
|
function findRuntimeLauncher(runtime) {
|
|
@@ -139,6 +142,83 @@ async function countUserMessages(transcriptPath) {
|
|
|
139
142
|
}
|
|
140
143
|
return count;
|
|
141
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Observability P2: commit-diff acted_on signal.
|
|
147
|
+
* 세션 시작 이후 생성된 commit 들의 diff 를 scan 하여 surfaced 솔루션과 키워드 매칭.
|
|
148
|
+
* 프라이버시: commit sha 는 SHA1 해시 12char prefix 만 저장, diff/path 내용 미저장.
|
|
149
|
+
*/
|
|
150
|
+
async function scanCommitDiffForActedOn(sessionId, cwd, sessionStartTime) {
|
|
151
|
+
try {
|
|
152
|
+
const surfaces = querySurfacedWithin(sessionId, 30); // 30분 window
|
|
153
|
+
if (surfaces.length === 0)
|
|
154
|
+
return;
|
|
155
|
+
const since = new Date(sessionStartTime).toISOString();
|
|
156
|
+
let logOutput;
|
|
157
|
+
try {
|
|
158
|
+
logOutput = execFileSync('git', ['log', '--since', since, '--pretty=format:%H'], {
|
|
159
|
+
cwd, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return; // git not available or not a repo
|
|
164
|
+
}
|
|
165
|
+
const shas = logOutput.trim().split('\n').filter(Boolean);
|
|
166
|
+
if (shas.length === 0)
|
|
167
|
+
return;
|
|
168
|
+
const seen = new Set(); // (sha12+solutionId) dedup
|
|
169
|
+
for (const sha of shas) {
|
|
170
|
+
let diff;
|
|
171
|
+
try {
|
|
172
|
+
diff = execFileSync('git', ['show', '--unified=0', sha], {
|
|
173
|
+
cwd, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const diffLower = diff.toLowerCase();
|
|
180
|
+
const shaHash = crypto.createHash('sha1').update(sha).digest('hex').slice(0, 12);
|
|
181
|
+
for (const surf of surfaces) {
|
|
182
|
+
const dedupKey = `${shaHash}:${surf.solutionId}`;
|
|
183
|
+
if (seen.has(dedupKey))
|
|
184
|
+
continue;
|
|
185
|
+
const filePath = path.join(ME_SOLUTIONS, `${surf.solutionId}.md`);
|
|
186
|
+
if (!fs.existsSync(filePath))
|
|
187
|
+
continue;
|
|
188
|
+
let raw;
|
|
189
|
+
try {
|
|
190
|
+
raw = fs.readFileSync(filePath, 'utf-8');
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const sol = parseSolutionV3(raw);
|
|
196
|
+
if (!sol)
|
|
197
|
+
continue;
|
|
198
|
+
const tags = sol.frontmatter.tags ?? [];
|
|
199
|
+
const identifiers = sol.frontmatter.identifiers ?? [];
|
|
200
|
+
if (tags.length === 0 && identifiers.length === 0)
|
|
201
|
+
continue;
|
|
202
|
+
const hit = tags.some(t => diffLower.includes(t.toLowerCase()))
|
|
203
|
+
|| identifiers.some(id => diffLower.includes(id.toLowerCase()));
|
|
204
|
+
if (!hit)
|
|
205
|
+
continue;
|
|
206
|
+
seen.add(dedupKey);
|
|
207
|
+
emitSolutionEvent({
|
|
208
|
+
sessionId,
|
|
209
|
+
solutionId: surf.solutionId,
|
|
210
|
+
eventType: 'acted_on',
|
|
211
|
+
signalSource: 'commit-diff',
|
|
212
|
+
signalScore: 0.30,
|
|
213
|
+
meta: { commit_sha_hash: shaHash, surface_ts: surf.ts },
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
log.debug('scanCommitDiffForActedOn 실패', e);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
142
222
|
/**
|
|
143
223
|
* 세션 종료 후 자동 compound 추출 + USER.md 업데이트.
|
|
144
224
|
* auto-compound-runner.ts를 동기 실행하여 솔루션 추출 + 사용자 패턴 관찰.
|
|
@@ -160,16 +240,96 @@ async function runAutoCompound(cwd, transcriptPath, sessionId) {
|
|
|
160
240
|
}
|
|
161
241
|
/**
|
|
162
242
|
* Transcript를 SQLite FTS5에 인덱싱 (추후 session-search MCP 도구용).
|
|
243
|
+
*
|
|
244
|
+
* v0.4.8 (A1): runtime 별 schema 차이로 분기. Claude 는 `entry.type === 'user'|
|
|
245
|
+
* 'assistant'`, Codex 는 `entry.type === 'response_item' && entry.payload.role`.
|
|
163
246
|
*/
|
|
164
|
-
async function indexTranscriptToFTS(cwd, transcriptPath, sessionId) {
|
|
247
|
+
async function indexTranscriptToFTS(cwd, transcriptPath, sessionId, runtime = 'claude') {
|
|
165
248
|
try {
|
|
166
|
-
const
|
|
167
|
-
|
|
249
|
+
const store = await import('./session-store.js');
|
|
250
|
+
if (runtime === 'codex') {
|
|
251
|
+
await store.indexCodexSession(cwd, transcriptPath, sessionId);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
await store.indexSession(cwd, transcriptPath, sessionId);
|
|
255
|
+
}
|
|
168
256
|
}
|
|
169
257
|
catch (e) {
|
|
170
258
|
log.debug('FTS5 인덱싱 실패 (session-store 미구현 시 정상)', e);
|
|
171
259
|
}
|
|
172
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Plan B-1: 세션 transcript 사후 스캔으로 rate-limit 감지.
|
|
263
|
+
*
|
|
264
|
+
* stdio='inherit' 라 Claude 출력을 직접 캡처할 수 없으므로,
|
|
265
|
+
* 세션 종료 후 transcript JSONL 의 마지막 N 라인을 읽어
|
|
266
|
+
* RATE_LIMIT_REGEX 매칭 여부를 확인한다.
|
|
267
|
+
*
|
|
268
|
+
* - transcript 없음 / 빈 파일 / parse 실패 → matched: false (fail-open)
|
|
269
|
+
* - 이미 pending-resume.json 존재 시 hook 이 먼저 잡은 것이므로 덮어쓰지 않음
|
|
270
|
+
*
|
|
271
|
+
* @param transcriptPath JSONL 파일 경로
|
|
272
|
+
* @param tailLines 검사할 마지막 라인 수 (기본 5)
|
|
273
|
+
*/
|
|
274
|
+
export async function scanTranscriptForRateLimit(transcriptPath, tailLines = 5) {
|
|
275
|
+
const notFound = { matched: false, resetAt: null };
|
|
276
|
+
if (!transcriptPath)
|
|
277
|
+
return notFound;
|
|
278
|
+
try {
|
|
279
|
+
if (!fs.existsSync(transcriptPath))
|
|
280
|
+
return notFound;
|
|
281
|
+
const { RATE_LIMIT_REGEX, parseRateLimitResetAt } = await import('../hooks/context-guard.js');
|
|
282
|
+
// tail: 파일 끝에서 tailLines 개 라인만 읽음 (대용량 transcript 효율)
|
|
283
|
+
const { createInterface } = await import('node:readline');
|
|
284
|
+
const stream = fs.createReadStream(transcriptPath, { encoding: 'utf-8' });
|
|
285
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
286
|
+
const lines = [];
|
|
287
|
+
for await (const line of rl) {
|
|
288
|
+
if (line)
|
|
289
|
+
lines.push(line);
|
|
290
|
+
if (lines.length > tailLines)
|
|
291
|
+
lines.shift();
|
|
292
|
+
}
|
|
293
|
+
rl.close();
|
|
294
|
+
stream.close();
|
|
295
|
+
if (lines.length === 0)
|
|
296
|
+
return notFound;
|
|
297
|
+
// 각 라인의 content/text/message 필드를 합쳐 regex 적용
|
|
298
|
+
const combined = lines
|
|
299
|
+
.map((line) => {
|
|
300
|
+
try {
|
|
301
|
+
const obj = JSON.parse(line);
|
|
302
|
+
const parts = [];
|
|
303
|
+
if (typeof obj.content === 'string')
|
|
304
|
+
parts.push(obj.content);
|
|
305
|
+
if (typeof obj.text === 'string')
|
|
306
|
+
parts.push(obj.text);
|
|
307
|
+
if (typeof obj.message === 'string')
|
|
308
|
+
parts.push(obj.message);
|
|
309
|
+
// content 가 배열인 경우 (Claude JSONL block format)
|
|
310
|
+
if (Array.isArray(obj.content)) {
|
|
311
|
+
for (const block of obj.content) {
|
|
312
|
+
if (block && typeof block.text === 'string') {
|
|
313
|
+
parts.push(block.text);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return parts.join(' ');
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return '';
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
.join(' ');
|
|
324
|
+
if (!RATE_LIMIT_REGEX.test(combined))
|
|
325
|
+
return notFound;
|
|
326
|
+
const resetAt = parseRateLimitResetAt(combined);
|
|
327
|
+
return { matched: true, resetAt };
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return notFound;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
173
333
|
/** Claude Code를 하네스 환경으로 실행. exit code를 반환. */
|
|
174
334
|
export async function spawnClaude(args, context, runtime = 'claude') {
|
|
175
335
|
const launcher = findRuntimeLauncher(runtime);
|
|
@@ -225,11 +385,34 @@ export async function spawnClaude(args, context, runtime = 'claude') {
|
|
|
225
385
|
else {
|
|
226
386
|
sessionId = path.basename(transcript, '.jsonl');
|
|
227
387
|
}
|
|
228
|
-
// 1. FTS5 인덱싱
|
|
229
|
-
|
|
230
|
-
|
|
388
|
+
// 1. FTS5 인덱싱 — v0.4.8 (A1) 부터 Claude/Codex 모두 지원.
|
|
389
|
+
await indexTranscriptToFTS(context.cwd, transcript, sessionId, runtime);
|
|
390
|
+
// 2. Plan B-1: transcript 사후 스캔으로 rate-limit 감지 (hook 미감지 보완)
|
|
391
|
+
const resumePath = path.join(STATE_DIR, 'pending-resume.json');
|
|
392
|
+
if (!fs.existsSync(resumePath)) {
|
|
393
|
+
try {
|
|
394
|
+
const scanResult = await scanTranscriptForRateLimit(transcript);
|
|
395
|
+
if (scanResult.matched) {
|
|
396
|
+
const marker = {
|
|
397
|
+
reason: 'rate-limit',
|
|
398
|
+
sessionId,
|
|
399
|
+
runtime,
|
|
400
|
+
resetAt: scanResult.resetAt,
|
|
401
|
+
savedAt: new Date().toISOString(),
|
|
402
|
+
cwd: context.cwd,
|
|
403
|
+
source: 'spawn-transcript-scan',
|
|
404
|
+
};
|
|
405
|
+
fs.writeFileSync(resumePath, JSON.stringify(marker, null, 2));
|
|
406
|
+
log.debug(`transcript scan: rate-limit 감지 → pending-resume.json 작성`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
catch (e) {
|
|
410
|
+
log.debug('transcript rate-limit scan 실패 (fail-open)', e);
|
|
411
|
+
}
|
|
231
412
|
}
|
|
232
|
-
//
|
|
413
|
+
// 3. Observability P2: commit-diff acted_on scan
|
|
414
|
+
await scanCommitDiffForActedOn(sessionId, context.cwd, sessionStartTime);
|
|
415
|
+
// 4. 자동 compound (10+ user 메시지인 경우만) — 양 runtime 호환
|
|
233
416
|
const userMsgCount = await countUserMessages(transcript);
|
|
234
417
|
if (userMsgCount >= 10) {
|
|
235
418
|
await runAutoCompound(context.cwd, transcript, sessionId);
|
|
@@ -17,6 +17,7 @@ import { execSync } from 'node:child_process';
|
|
|
17
17
|
import { loadActiveRules } from '../store/rule-store.js';
|
|
18
18
|
import { getUsageStats } from './usage-telemetry.js';
|
|
19
19
|
import { STATE_DIR } from './paths.js';
|
|
20
|
+
import { classifySolutions } from './lifecycle-classifier.js';
|
|
20
21
|
// 0.4.6 perf #13 — statusline 출력을 5초 캐싱.
|
|
21
22
|
// claude statusLine 은 짧은 간격으로 재호출되는데 매번 git/find/rule-store 를
|
|
22
23
|
// 실행하면 ~100ms 누적. CACHE_TTL_MS 동안 동일 출력 재사용.
|
|
@@ -123,6 +124,30 @@ function buildLine1(payload, cwd) {
|
|
|
123
124
|
parts.push(`${GREEN}${gitBranch}${RESET}`);
|
|
124
125
|
return parts.join(` ${DIM}|${RESET} `);
|
|
125
126
|
}
|
|
127
|
+
/** Build lifecycle line: "🔥X 🟡X 🥶X 💀X 🌱X" — P3 신설. 0건이면 null */
|
|
128
|
+
function buildLifecycleLine() {
|
|
129
|
+
try {
|
|
130
|
+
const classified = classifySolutions();
|
|
131
|
+
if (classified.length === 0)
|
|
132
|
+
return null;
|
|
133
|
+
const counts = { hot: 0, warm: 0, cold: 0, dead: 0, new: 0 };
|
|
134
|
+
for (const c of classified)
|
|
135
|
+
counts[c.lifecycle]++;
|
|
136
|
+
const total = counts.hot + counts.warm + counts.cold + counts.dead + counts.new;
|
|
137
|
+
if (total === 0)
|
|
138
|
+
return null;
|
|
139
|
+
return [
|
|
140
|
+
`${YELLOW}🔥${counts.hot}${RESET}`,
|
|
141
|
+
`${YELLOW}🟡${counts.warm}${RESET}`,
|
|
142
|
+
`${DIM}🥶${counts.cold}${RESET}`,
|
|
143
|
+
`${DIM}💀${counts.dead}${RESET}`,
|
|
144
|
+
`${DIM}🌱${counts.new}${RESET}`,
|
|
145
|
+
].join(` `);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
126
151
|
/** Build usage line: "📊 87/5h · 412/wk (claude)" — 0.4.6 신설 */
|
|
127
152
|
function buildUsageLine() {
|
|
128
153
|
try {
|
|
@@ -193,6 +218,7 @@ export async function handleStatusline() {
|
|
|
193
218
|
const line1 = buildLine1(payload, cwd);
|
|
194
219
|
const line3 = buildLine3(claudeDir, cwd);
|
|
195
220
|
const usageLine = buildUsageLine();
|
|
221
|
+
const lifecycleLine = buildLifecycleLine();
|
|
196
222
|
// Line 2 (context/usage): stdin JSON spec 미확인으로 생략 — TODO
|
|
197
223
|
// Line 4 (tool counts): 추적 인프라 없음 — TODO
|
|
198
224
|
// Line 5 (active task): 추적 인프라 없음 — TODO
|
|
@@ -200,6 +226,13 @@ export async function handleStatusline() {
|
|
|
200
226
|
console.log(line3);
|
|
201
227
|
if (usageLine)
|
|
202
228
|
console.log(usageLine);
|
|
203
|
-
|
|
229
|
+
if (lifecycleLine)
|
|
230
|
+
console.log(lifecycleLine);
|
|
231
|
+
const cacheLines = [line1, line3];
|
|
232
|
+
if (usageLine)
|
|
233
|
+
cacheLines.push(usageLine);
|
|
234
|
+
if (lifecycleLine)
|
|
235
|
+
cacheLines.push(lifecycleLine);
|
|
236
|
+
const cacheBody = cacheLines.join('\n') + '\n';
|
|
204
237
|
writeCache(cacheBody);
|
|
205
238
|
}
|
|
@@ -18,6 +18,13 @@ export declare function ensureV1Directories(): void;
|
|
|
18
18
|
export interface V1BootstrapResult {
|
|
19
19
|
needsOnboarding: boolean;
|
|
20
20
|
legacyBackupPath: string | null;
|
|
21
|
+
/**
|
|
22
|
+
* profile.json 이 존재했지만 parse/shape 오류로 사용 불가하여 v0.4.8
|
|
23
|
+
* 의 auto-repair 가 timestamp 백업으로 치워둔 경로. legacyBackupPath
|
|
24
|
+
* 와 의미가 다름 (legacy = model_version 구 버전 cutover, corrupt =
|
|
25
|
+
* 깨진 파일 격리).
|
|
26
|
+
*/
|
|
27
|
+
corruptProfileBackupPath: string | null;
|
|
21
28
|
session: SessionEffectiveState | null;
|
|
22
29
|
renderedRules: string | null;
|
|
23
30
|
profile: Profile | null;
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
import * as fs from 'node:fs';
|
|
16
16
|
import * as path from 'node:path';
|
|
17
17
|
import * as crypto from 'node:crypto';
|
|
18
|
-
import { FORGEN_HOME, ME_DIR, ME_RULES, ME_BEHAVIOR, V1_RECOMMENDATIONS_DIR, V1_SESSIONS_DIR, STATE_DIR, V1_RAW_LOGS_DIR, ME_SOLUTIONS } from './paths.js';
|
|
18
|
+
import { FORGEN_HOME, ME_DIR, ME_RULES, ME_BEHAVIOR, V1_RECOMMENDATIONS_DIR, V1_SESSIONS_DIR, STATE_DIR, V1_RAW_LOGS_DIR, ME_SOLUTIONS, SESSIONS_DIR } from './paths.js';
|
|
19
19
|
import { checkLegacyProfile, runLegacyCutover } from './legacy-detector.js';
|
|
20
20
|
import { detectRuntimeCapability } from './runtime-detector.js';
|
|
21
|
-
import { loadProfile, profileExists } from '../store/profile-store.js';
|
|
21
|
+
import { backupCorruptProfile, loadProfile, profileExists } from '../store/profile-store.js';
|
|
22
22
|
import { loadActiveRules, cleanupStaleSessionRules, markRulesInjected } from '../store/rule-store.js';
|
|
23
23
|
import { composeSession } from '../preset/preset-manager.js';
|
|
24
24
|
import { renderRules, DEFAULT_CONTEXT } from '../renderer/rule-renderer.js';
|
|
@@ -27,7 +27,14 @@ import { loadEvidenceBySession } from '../store/evidence-store.js';
|
|
|
27
27
|
import { computeSessionSignals, detectMismatch } from '../forge/mismatch-detector.js';
|
|
28
28
|
import { createRecommendation, saveRecommendation } from '../store/recommendation-store.js';
|
|
29
29
|
// ── Directory Initialization ──
|
|
30
|
-
|
|
30
|
+
// v0.4.8 (A3): SESSIONS_DIR (~/.forgen/sessions/) 도 v1 bootstrap 보장 대상.
|
|
31
|
+
// 이전엔 V1_DIRS 에 누락되어 있어, prepareHarness step 11 (startSessionLog)
|
|
32
|
+
// 에 도달 못 하는 코드 경로 (예: forgen install 직접 실행) 에선 디렉토리가
|
|
33
|
+
// 끝까지 생성되지 않았음. legacy session log 와 v1 effective state 는
|
|
34
|
+
// 서로 다른 저장소 책임이라 두 dir 모두 명시 보장.
|
|
35
|
+
// - SESSIONS_DIR = ~/.forgen/sessions/ ← legacy session log (transcript-like)
|
|
36
|
+
// - V1_SESSIONS_DIR = ~/.forgen/state/sessions/ ← v1 effective state per session
|
|
37
|
+
const V1_DIRS = [FORGEN_HOME, ME_DIR, ME_RULES, ME_BEHAVIOR, V1_RECOMMENDATIONS_DIR, STATE_DIR, V1_SESSIONS_DIR, V1_RAW_LOGS_DIR, ME_SOLUTIONS, SESSIONS_DIR];
|
|
31
38
|
export function ensureV1Directories() {
|
|
32
39
|
for (const dir of V1_DIRS) {
|
|
33
40
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -48,6 +55,7 @@ export function bootstrapV1Session() {
|
|
|
48
55
|
return {
|
|
49
56
|
needsOnboarding: true,
|
|
50
57
|
legacyBackupPath,
|
|
58
|
+
corruptProfileBackupPath: null,
|
|
51
59
|
session: null,
|
|
52
60
|
renderedRules: null,
|
|
53
61
|
profile: null,
|
|
@@ -56,7 +64,20 @@ export function bootstrapV1Session() {
|
|
|
56
64
|
}
|
|
57
65
|
const profile = loadProfile();
|
|
58
66
|
if (!profile) {
|
|
59
|
-
|
|
67
|
+
// v0.4.8 — corrupt/invalid profile auto-repair.
|
|
68
|
+
// profileExists()=true && loadProfile()=null 은 parse 실패 또는 v1
|
|
69
|
+
// shape 위반. 그대로 두면 다음 실행에서도 같은 분기로 빠지므로
|
|
70
|
+
// backup 후 새 onboarding 흐름을 강제.
|
|
71
|
+
const corruptProfileBackupPath = backupCorruptProfile();
|
|
72
|
+
return {
|
|
73
|
+
needsOnboarding: true,
|
|
74
|
+
legacyBackupPath,
|
|
75
|
+
corruptProfileBackupPath,
|
|
76
|
+
session: null,
|
|
77
|
+
renderedRules: null,
|
|
78
|
+
profile: null,
|
|
79
|
+
mismatch: null,
|
|
80
|
+
};
|
|
60
81
|
}
|
|
61
82
|
// 3. Runtime capability 감지
|
|
62
83
|
const runtime = detectRuntimeCapability();
|
|
@@ -133,7 +154,7 @@ export function bootstrapV1Session() {
|
|
|
133
154
|
try {
|
|
134
155
|
// 세션 시작 로그
|
|
135
156
|
const rawLogPath = path.join(V1_RAW_LOGS_DIR, `${sessionId}.jsonl`);
|
|
136
|
-
fs.appendFileSync(rawLogPath, JSON.stringify({
|
|
157
|
+
fs.appendFileSync(rawLogPath, `${JSON.stringify({
|
|
137
158
|
event: 'session-started',
|
|
138
159
|
session_id: sessionId,
|
|
139
160
|
timestamp: new Date().toISOString(),
|
|
@@ -142,7 +163,7 @@ export function bootstrapV1Session() {
|
|
|
142
163
|
judgment_pack: profile.base_packs.judgment_pack,
|
|
143
164
|
communication_pack: profile.base_packs.communication_pack,
|
|
144
165
|
effective_trust: session.effective_trust_policy,
|
|
145
|
-
})
|
|
166
|
+
})}\n`);
|
|
146
167
|
// TTL sweep: 7일 이상 된 raw log 파일 삭제
|
|
147
168
|
const TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
148
169
|
const now = Date.now();
|
|
@@ -163,6 +184,7 @@ export function bootstrapV1Session() {
|
|
|
163
184
|
return {
|
|
164
185
|
needsOnboarding: false,
|
|
165
186
|
legacyBackupPath,
|
|
187
|
+
corruptProfileBackupPath: null,
|
|
166
188
|
session,
|
|
167
189
|
renderedRules,
|
|
168
190
|
profile,
|