@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.
Files changed (159) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +40 -0
  3. package/assets/dev-guide/be/README.md +226 -0
  4. package/assets/dev-guide/be/adapters/build-agents-md.sh +63 -0
  5. package/assets/dev-guide/be/principles/common.md +433 -0
  6. package/assets/dev-guide/be/principles/go.md +469 -0
  7. package/assets/dev-guide/be/principles/node.md +388 -0
  8. package/assets/dev-guide/be/skills/go/be-build/SKILL.md +262 -0
  9. package/assets/dev-guide/be/skills/go/be-perf/SKILL.md +308 -0
  10. package/assets/dev-guide/be/skills/go/be-review/SKILL.md +119 -0
  11. package/assets/dev-guide/be/skills/go/be-security/SKILL.md +362 -0
  12. package/assets/dev-guide/be/skills/node/be-build/SKILL.md +239 -0
  13. package/assets/dev-guide/be/skills/node/be-perf/SKILL.md +272 -0
  14. package/assets/dev-guide/be/skills/node/be-review/SKILL.md +118 -0
  15. package/assets/dev-guide/be/skills/node/be-security/SKILL.md +355 -0
  16. package/assets/dev-guide/be/sources/12factor/INDEX.md +53 -0
  17. package/assets/dev-guide/be/sources/api-design/INDEX.md +56 -0
  18. package/assets/dev-guide/be/sources/ddia/INDEX.md +55 -0
  19. package/assets/dev-guide/be/sources/go-runtime/INDEX.md +62 -0
  20. package/assets/dev-guide/be/sources/node-runtime/INDEX.md +60 -0
  21. package/assets/dev-guide/be/sources/otel/INDEX.md +53 -0
  22. package/assets/dev-guide/be/sources/owasp-api/INDEX.md +52 -0
  23. package/assets/dev-guide/be/sources/postgres/INDEX.md +55 -0
  24. package/assets/dev-guide/be/sources/sre-book/INDEX.md +48 -0
  25. package/assets/dev-guide/fe/README.md +197 -0
  26. package/assets/dev-guide/fe/adapters/build-agents-md.sh +63 -0
  27. package/assets/dev-guide/fe/adapters/refresh.sh +68 -0
  28. package/assets/dev-guide/fe/principles/common.md +160 -0
  29. package/assets/dev-guide/fe/principles/react.md +183 -0
  30. package/assets/dev-guide/fe/principles/vue.md +196 -0
  31. package/assets/dev-guide/fe/skills/react/fe-build/SKILL.md +139 -0
  32. package/assets/dev-guide/fe/skills/react/fe-perf/SKILL.md +179 -0
  33. package/assets/dev-guide/fe/skills/react/fe-review/SKILL.md +141 -0
  34. package/assets/dev-guide/fe/skills/vue/fe-build/SKILL.md +148 -0
  35. package/assets/dev-guide/fe/skills/vue/fe-perf/SKILL.md +163 -0
  36. package/assets/dev-guide/fe/skills/vue/fe-review/SKILL.md +136 -0
  37. package/assets/dev-guide/fe/sources/a11y-dx/INDEX.md +41 -0
  38. package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-memory.md +150 -0
  39. package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-performance.md +99 -0
  40. package/assets/dev-guide/fe/sources/a11y-dx/lighthouse-audits.md +146 -0
  41. package/assets/dev-guide/fe/sources/a11y-dx/react-devtools-profiler.md +128 -0
  42. package/assets/dev-guide/fe/sources/a11y-dx/wcag22-new-criteria.md +174 -0
  43. package/assets/dev-guide/fe/sources/perf/01-core-web-vitals.md +58 -0
  44. package/assets/dev-guide/fe/sources/perf/02-inp.md +83 -0
  45. package/assets/dev-guide/fe/sources/perf/03-lcp-cls.md +130 -0
  46. package/assets/dev-guide/fe/sources/perf/04-speculation-rules.md +148 -0
  47. package/assets/dev-guide/fe/sources/perf/05-view-transitions.md +153 -0
  48. package/assets/dev-guide/fe/sources/perf/06-nextjs-caching.md +188 -0
  49. package/assets/dev-guide/fe/sources/perf/07-server-components.md +181 -0
  50. package/assets/dev-guide/fe/sources/perf/08-ppr.md +133 -0
  51. package/assets/dev-guide/fe/sources/perf/09-nextjs-image.md +200 -0
  52. package/assets/dev-guide/fe/sources/perf/10-optimize-lcp.md +201 -0
  53. package/assets/dev-guide/fe/sources/perf/INDEX.md +88 -0
  54. package/assets/dev-guide/fe/sources/react/INDEX.md +41 -0
  55. package/assets/dev-guide/fe/sources/react/keeping-components-pure.md +135 -0
  56. package/assets/dev-guide/fe/sources/react/no-effect-patterns.md +183 -0
  57. package/assets/dev-guide/fe/sources/react/react-compiler.md +182 -0
  58. package/assets/dev-guide/fe/sources/react/server-components.md +194 -0
  59. package/assets/dev-guide/fe/sources/react/server-functions.md +192 -0
  60. package/assets/dev-guide/fe/sources/react/suspense.md +218 -0
  61. package/assets/dev-guide/fe/sources/react/use-action-state.md +123 -0
  62. package/assets/dev-guide/fe/sources/react/use-form-status.md +158 -0
  63. package/assets/dev-guide/fe/sources/react/use-hook.md +153 -0
  64. package/assets/dev-guide/fe/sources/react/use-optimistic.md +194 -0
  65. package/assets/dev-guide/fe/sources/toss-ff/INDEX.md +58 -0
  66. package/assets/dev-guide/fe/sources/toss-ff/cohesion-code-directory.md +79 -0
  67. package/assets/dev-guide/fe/sources/toss-ff/cohesion-form-fields.md +110 -0
  68. package/assets/dev-guide/fe/sources/toss-ff/cohesion-magic-number.md +47 -0
  69. package/assets/dev-guide/fe/sources/toss-ff/coupling-item-edit-modal.md +124 -0
  70. package/assets/dev-guide/fe/sources/toss-ff/coupling-use-bottom-sheet.md +57 -0
  71. package/assets/dev-guide/fe/sources/toss-ff/coupling-use-page-state.md +71 -0
  72. package/assets/dev-guide/fe/sources/toss-ff/overview-4-principles.md +77 -0
  73. package/assets/dev-guide/fe/sources/toss-ff/predictability-hidden-logic.md +59 -0
  74. package/assets/dev-guide/fe/sources/toss-ff/predictability-http.md +77 -0
  75. package/assets/dev-guide/fe/sources/toss-ff/predictability-use-user.md +110 -0
  76. package/assets/dev-guide/fe/sources/toss-ff/readability-comparison-order.md +52 -0
  77. package/assets/dev-guide/fe/sources/toss-ff/readability-condition-name.md +64 -0
  78. package/assets/dev-guide/fe/sources/toss-ff/readability-login-start-page.md +183 -0
  79. package/assets/dev-guide/fe/sources/toss-ff/readability-magic-number.md +53 -0
  80. package/assets/dev-guide/fe/sources/toss-ff/readability-submit-button.md +73 -0
  81. package/assets/dev-guide/fe/sources/toss-ff/readability-ternary-operator.md +38 -0
  82. package/assets/dev-guide/fe/sources/toss-ff/readability-use-page-state.md +77 -0
  83. package/assets/dev-guide/fe/sources/toss-ff/readability-user-policy.md +98 -0
  84. package/assets/dev-guide/fe/sources/vue/INDEX.md +17 -0
  85. package/assets/dev-guide/fe/sources/vue/composition-api.md +251 -0
  86. package/assets/dev-guide/fe/sources/vue/nuxt-data-fetching.md +232 -0
  87. package/assets/dev-guide/fe/sources/vue/pinia-state-management.md +134 -0
  88. package/assets/dev-guide/fe/sources/vue/reactivity-pitfalls.md +261 -0
  89. package/assets/dev-guide/fe/sources/vue/style-guide-priority-a.md +117 -0
  90. package/assets/dev-guide/fe/sources/vue/style-guide-priority-b.md +231 -0
  91. package/assets/dev-guide/fe/sources/vue/style-guide-priority-c.md +86 -0
  92. package/assets/dev-guide/fe/sources/vue/style-guide-priority-d.md +72 -0
  93. package/dist/checks/self-score-deflation.js +6 -4
  94. package/dist/cli.js +47 -2
  95. package/dist/core/auto-compound-runner.js +6 -2
  96. package/dist/core/dashboard-cli.d.ts +12 -0
  97. package/dist/core/dashboard-cli.js +226 -0
  98. package/dist/core/dashboard.js +2 -2
  99. package/dist/core/dev-guide-injector.d.ts +26 -0
  100. package/dist/core/dev-guide-injector.js +137 -0
  101. package/dist/core/doctor.d.ts +10 -0
  102. package/dist/core/doctor.js +49 -8
  103. package/dist/core/harness.js +8 -2
  104. package/dist/core/init.js +53 -0
  105. package/dist/core/inspect-cli.js +4 -4
  106. package/dist/core/lifecycle-classifier.d.ts +23 -0
  107. package/dist/core/lifecycle-classifier.js +104 -0
  108. package/dist/core/migrate-evidence-host.js +1 -1
  109. package/dist/core/notify.js +7 -0
  110. package/dist/core/observability-backfill.d.ts +31 -0
  111. package/dist/core/observability-backfill.js +178 -0
  112. package/dist/core/observability-store.d.ts +58 -0
  113. package/dist/core/observability-store.js +195 -0
  114. package/dist/core/paths.d.ts +16 -2
  115. package/dist/core/paths.js +16 -2
  116. package/dist/core/session-store.d.ts +12 -1
  117. package/dist/core/session-store.js +77 -1
  118. package/dist/core/spawn.d.ts +17 -0
  119. package/dist/core/spawn.js +191 -8
  120. package/dist/core/statusline-cli.js +34 -1
  121. package/dist/core/v1-bootstrap.d.ts +7 -0
  122. package/dist/core/v1-bootstrap.js +28 -6
  123. package/dist/engine/compound-extractor.js +40 -1
  124. package/dist/engine/compound-loop.js +6 -0
  125. package/dist/engine/compound-retire.d.ts +20 -0
  126. package/dist/engine/compound-retire.js +85 -0
  127. package/dist/engine/learn-cli.js +2 -2
  128. package/dist/engine/lifecycle/bypass-detector.js +3 -2
  129. package/dist/engine/lifecycle/meta-reclassifier.js +1 -1
  130. package/dist/engine/lifecycle/signals.js +2 -2
  131. package/dist/engine/lifecycle/trigger-t1-correction.js +1 -1
  132. package/dist/engine/solution-candidate.js +1 -1
  133. package/dist/engine/solution-outcomes.js +1 -1
  134. package/dist/engine/solution-quarantine.js +1 -1
  135. package/dist/engine/solution-weakness.js +8 -2
  136. package/dist/forge/cli.js +1 -1
  137. package/dist/hooks/context-guard.js +25 -1
  138. package/dist/hooks/keyword-detector.js +1 -1
  139. package/dist/hooks/post-tool-use.js +48 -0
  140. package/dist/hooks/secret-filter.js +2 -2
  141. package/dist/hooks/shared/hook-response.js +1 -1
  142. package/dist/hooks/shared/hook-timing.js +3 -3
  143. package/dist/hooks/solution-injector.js +94 -1
  144. package/dist/hooks/stop-guard.js +3 -3
  145. package/dist/host/install-claude.d.ts +6 -2
  146. package/dist/host/install-claude.js +74 -2
  147. package/dist/host/install-codex.d.ts +4 -0
  148. package/dist/host/install-codex.js +72 -1
  149. package/dist/host/install-orchestrator.js +1 -0
  150. package/dist/mcp/tools.js +1 -1
  151. package/dist/preset/facet-catalog.js +2 -2
  152. package/dist/renderer/rule-renderer.js +7 -7
  153. package/dist/store/compound-usage-store.js +1 -1
  154. package/dist/store/implicit-feedback-store.js +2 -2
  155. package/dist/store/profile-store.d.ts +11 -0
  156. package/dist/store/profile-store.js +23 -0
  157. package/package.json +6 -6
  158. package/plugin.json +1 -1
  159. package/scripts/postinstall.js +134 -0
@@ -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
- /** ~/.forgen/sessions/ — 세션 로그 */
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
- /** ~/.forgen/state/sessions/ — Session Effective State */
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;
@@ -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
- /** ~/.forgen/sessions/ — 세션 로그 */
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
- /** ~/.forgen/state/sessions/ — Session Effective State */
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
- * Transcript JSONL을 SQLite에 인덱싱.
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();
@@ -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;
@@ -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 { indexSession } = await import('./session-store.js');
167
- await indexSession(cwd, transcriptPath, sessionId);
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 인덱싱 (claude only codex schema FTS 호환은 미검증, 별도 작업)
229
- if (runtime === 'claude') {
230
- await indexTranscriptToFTS(context.cwd, transcript, sessionId);
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
- // 2. 자동 compound (10+ user 메시지인 경우만) — 양 runtime 호환
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
- const cacheBody = usageLine ? `${line1}\n${line3}\n${usageLine}\n` : `${line1}\n${line3}\n`;
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
- 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];
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
- return { needsOnboarding: true, legacyBackupPath, session: null, renderedRules: null, profile: null, mismatch: null };
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
- }) + '\n');
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,