@wooojin/forgen 0.4.13 → 0.5.5

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 (166) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +261 -0
  3. package/README.ja.md +16 -12
  4. package/README.ko.md +12 -8
  5. package/README.md +90 -27
  6. package/README.zh.md +16 -12
  7. package/assets/claude/commands/forge-loop.md +7 -1
  8. package/assets/claude/commands/ship.md +18 -1
  9. package/assets/opencode/forgen.ts +52 -0
  10. package/assets/shared/hook-registry.json +210 -22
  11. package/dist/checks/_shared/meta-guard-dispatch.d.ts +7 -0
  12. package/dist/checks/_shared/meta-guard-dispatch.js +12 -2
  13. package/dist/checks/_shared/model-profile.d.ts +25 -0
  14. package/dist/checks/_shared/model-profile.js +63 -0
  15. package/dist/cli.js +99 -153
  16. package/dist/core/auto-compound-runner.d.ts +0 -11
  17. package/dist/core/auto-compound-runner.js +199 -92
  18. package/dist/core/changelog-cli.d.ts +4 -1
  19. package/dist/core/changelog-cli.js +8 -7
  20. package/dist/core/compound-consent.d.ts +15 -0
  21. package/dist/core/compound-consent.js +48 -0
  22. package/dist/core/compound-sweep-cli.d.ts +60 -0
  23. package/dist/core/compound-sweep-cli.js +374 -0
  24. package/dist/core/config-injector.d.ts +16 -1
  25. package/dist/core/config-injector.js +47 -28
  26. package/dist/core/dashboard-cli.js +40 -16
  27. package/dist/core/dashboard.d.ts +3 -4
  28. package/dist/core/dashboard.js +15 -34
  29. package/dist/core/dev-cli.d.ts +13 -0
  30. package/dist/core/dev-cli.js +70 -0
  31. package/dist/core/doctor.d.ts +16 -5
  32. package/dist/core/doctor.js +190 -176
  33. package/dist/core/drift-score.d.ts +2 -0
  34. package/dist/core/drift-score.js +9 -1
  35. package/dist/core/harness.js +115 -43
  36. package/dist/core/health-cli.d.ts +2 -0
  37. package/dist/core/health-cli.js +6 -1
  38. package/dist/core/host-detect.d.ts +3 -1
  39. package/dist/core/host-detect.js +26 -1
  40. package/dist/core/migrate-cli.js +15 -0
  41. package/dist/core/migrate-evidence-host.d.ts +2 -1
  42. package/dist/core/migrate-tenetx.d.ts +50 -0
  43. package/dist/core/migrate-tenetx.js +262 -0
  44. package/dist/core/probe-workflow-cli.d.ts +3 -3
  45. package/dist/core/probe-workflow-cli.js +13 -13
  46. package/dist/core/recall-cli.js +1 -1
  47. package/dist/core/regress-map-cli.js +1 -1
  48. package/dist/core/rendered-rules-manifest.d.ts +29 -0
  49. package/dist/core/rendered-rules-manifest.js +60 -0
  50. package/dist/core/session-store.js +14 -3
  51. package/dist/core/settings-injector.d.ts +3 -0
  52. package/dist/core/settings-injector.js +12 -16
  53. package/dist/core/spawn.d.ts +37 -1
  54. package/dist/core/spawn.js +116 -9
  55. package/dist/core/state-gc.js +1 -0
  56. package/dist/core/status-cli.d.ts +20 -0
  57. package/dist/core/status-cli.js +100 -0
  58. package/dist/core/statusline-cli.d.ts +7 -0
  59. package/dist/core/statusline-cli.js +63 -19
  60. package/dist/core/transcript-summary.d.ts +18 -0
  61. package/dist/core/transcript-summary.js +81 -0
  62. package/dist/core/trust-layer-intent.d.ts +21 -1
  63. package/dist/core/trust-layer-intent.js +7 -0
  64. package/dist/core/types.d.ts +3 -2
  65. package/dist/core/uninstall.js +12 -0
  66. package/dist/core/usage-telemetry.d.ts +7 -1
  67. package/dist/core/usage-telemetry.js +7 -9
  68. package/dist/core/v1-bootstrap.js +1 -1
  69. package/dist/core/watch-cli.js +1 -1
  70. package/dist/engine/compound-extractor.js +10 -0
  71. package/dist/engine/compound-loop.js +55 -7
  72. package/dist/engine/compound-share.d.ts +85 -0
  73. package/dist/engine/compound-share.js +606 -0
  74. package/dist/engine/correction-cluster-runner.d.ts +38 -0
  75. package/dist/engine/correction-cluster-runner.js +188 -0
  76. package/dist/engine/correction-clustering.d.ts +80 -0
  77. package/dist/engine/correction-clustering.js +167 -0
  78. package/dist/engine/enforce-classifier.d.ts +10 -1
  79. package/dist/engine/enforce-classifier.js +111 -22
  80. package/dist/engine/extraction-session.js +10 -3
  81. package/dist/engine/private-filter.d.ts +36 -0
  82. package/dist/engine/private-filter.js +100 -0
  83. package/dist/engine/ranking-pipeline.js +4 -2
  84. package/dist/engine/relevance-gate.d.ts +12 -0
  85. package/dist/engine/relevance-gate.js +12 -0
  86. package/dist/engine/roi-demotion.d.ts +79 -0
  87. package/dist/engine/roi-demotion.js +159 -0
  88. package/dist/engine/solution-format.d.ts +1 -0
  89. package/dist/engine/solution-format.js +26 -0
  90. package/dist/engine/solution-matcher.js +10 -1
  91. package/dist/fgx.js +7 -6
  92. package/dist/forge/cli.js +8 -2
  93. package/dist/hooks/compound-reflection.js +6 -1
  94. package/dist/hooks/context-guard.d.ts +16 -1
  95. package/dist/hooks/context-guard.js +99 -49
  96. package/dist/hooks/hook-config.js +5 -0
  97. package/dist/hooks/hook-registry.d.ts +6 -1
  98. package/dist/hooks/hooks-generator.js +3 -0
  99. package/dist/hooks/post-tool-use.js +2 -3
  100. package/dist/hooks/pre-compact.js +14 -0
  101. package/dist/hooks/pre-tool-use.js +5 -1
  102. package/dist/hooks/session-end.d.ts +34 -0
  103. package/dist/hooks/session-end.js +100 -0
  104. package/dist/hooks/session-recovery.js +18 -1
  105. package/dist/hooks/shared/hook-timing.js +6 -1
  106. package/dist/hooks/shared/stop-triggers.d.ts +29 -2
  107. package/dist/hooks/shared/stop-triggers.js +35 -2
  108. package/dist/hooks/solution-injector.d.ts +28 -0
  109. package/dist/hooks/solution-injector.js +126 -28
  110. package/dist/hooks/stop-guard.js +5 -2
  111. package/dist/hooks/subagent-stop-guard.js +4 -1
  112. package/dist/host/capabilities-claude.js +1 -0
  113. package/dist/host/capabilities-codex.js +1 -0
  114. package/dist/host/capabilities-opencode.d.ts +26 -0
  115. package/dist/host/capabilities-opencode.js +78 -0
  116. package/dist/host/capabilities-registry.d.ts +7 -0
  117. package/dist/host/capabilities-registry.js +14 -0
  118. package/dist/host/codex-rules-context.d.ts +25 -0
  119. package/dist/host/codex-rules-context.js +62 -0
  120. package/dist/host/exec-host.d.ts +14 -3
  121. package/dist/host/exec-host.js +15 -2
  122. package/dist/host/host-binding.d.ts +27 -0
  123. package/dist/host/host-binding.js +11 -0
  124. package/dist/host/host-runtime.js +18 -0
  125. package/dist/host/install-codex.d.ts +66 -0
  126. package/dist/host/install-codex.js +293 -25
  127. package/dist/host/install-opencode.d.ts +38 -0
  128. package/dist/host/install-opencode.js +148 -0
  129. package/dist/host/install-orchestrator.d.ts +4 -1
  130. package/dist/host/install-orchestrator.js +30 -1
  131. package/dist/host/invoke-agent.d.ts +3 -2
  132. package/dist/host/invoke-agent.js +1 -0
  133. package/dist/host/opencode/context-cli.d.ts +15 -0
  134. package/dist/host/opencode/context-cli.js +25 -0
  135. package/dist/host/opencode/guard-cli.d.ts +13 -0
  136. package/dist/host/opencode/guard-cli.js +39 -0
  137. package/dist/host/opencode/plugin/forgen.d.ts +31 -0
  138. package/dist/host/opencode/plugin/forgen.js +60 -0
  139. package/dist/host/opencode/translate.d.ts +49 -0
  140. package/dist/host/opencode/translate.js +96 -0
  141. package/dist/host/parity-harness.d.ts +10 -2
  142. package/dist/host/parity-harness.js +7 -3
  143. package/dist/host/projection.d.ts +37 -3
  144. package/dist/host/projection.js +104 -42
  145. package/dist/mcp/tools.js +17 -4
  146. package/dist/store/evidence-store.d.ts +2 -6
  147. package/dist/store/evidence-store.js +164 -46
  148. package/dist/store/host-mismatch.d.ts +2 -1
  149. package/dist/store/host-mismatch.js +8 -8
  150. package/dist/store/profile-store.d.ts +3 -2
  151. package/dist/store/rule-store.d.ts +41 -0
  152. package/dist/store/rule-store.js +77 -2
  153. package/dist/store/types.d.ts +9 -2
  154. package/hooks/hooks.json +13 -1
  155. package/package.json +3 -2
  156. package/plugin.json +2 -2
  157. package/skills/architecture-decision/SKILL.md +18 -0
  158. package/skills/calibrate/SKILL.md +18 -0
  159. package/skills/code-review/SKILL.md +17 -0
  160. package/skills/compound/SKILL.md +17 -0
  161. package/skills/deep-interview/SKILL.md +7 -0
  162. package/skills/docker/SKILL.md +18 -0
  163. package/skills/forge-loop/SKILL.md +30 -1
  164. package/skills/learn/SKILL.md +15 -0
  165. package/skills/retro/SKILL.md +16 -0
  166. package/skills/ship/SKILL.md +36 -1
@@ -1,5 +1,5 @@
1
1
  /**
2
- * forgen probe-workflow — ADR-009 §1 결정적 미확인 변수 실측 도구.
2
+ * forgen dev probe-workflow — ADR-009 §1 결정적 미확인 변수 실측 도구.
3
3
  *
4
4
  * 질문: dynamic workflow **내부** 에이전트에 대해 SubagentStart/Stop·PostToolUse
5
5
  * 훅이 발화하는가? (워크플로우 런타임은 "대화와 분리된 격리 백그라운드"로
@@ -16,9 +16,9 @@
16
16
  * - (보조) hook-timing.jsonl 의 event 별 엔트리
17
17
  *
18
18
  * 절차 (2단계 — 런타임이 격리되어 있어 단일 프로세스로는 트리거 불가):
19
- * 1. `forgen probe-workflow arm` → baseline 마커 기록 + 안내 출력
19
+ * 1. `forgen dev probe-workflow arm` → baseline 마커 기록 + 안내 출력
20
20
  * 2. 사용자가 Claude Code 에서 워크플로우 1회 실행 (그 사이 다른 작업 금지)
21
- * 3. `forgen probe-workflow report` → baseline 이후 신호 수집 → verdict 박제
21
+ * 3. `forgen dev probe-workflow report` → baseline 이후 신호 수집 → verdict 박제
22
22
  *
23
23
  * 가정 (detailed-communication): arm~report 사이에 사용자가 **워크플로우만**
24
24
  * 실행했다고 전제한다. 일반 Task-tool subagent 를 같이 돌리면 신호가 섞인다.
@@ -188,14 +188,14 @@ function armProbe() {
188
188
  fs.mkdirSync(STATE_DIR, { recursive: true });
189
189
  fs.writeFileSync(BASELINE_PATH, JSON.stringify(baseline, null, 2));
190
190
  console.log(`
191
- ${C.bold}forgen probe-workflow — armed${C.reset} ${C.dim}(${baseline.armedIso})${C.reset}
191
+ ${C.bold}forgen dev probe-workflow — armed${C.reset} ${C.dim}(${baseline.armedIso})${C.reset}
192
192
 
193
193
  ${C.cyan}다음을 정확히 순서대로 실행하세요:${C.reset}
194
194
  1. Claude Code (v2.1.154+, workflows 활성) 세션에서 ${C.bold}워크플로우 1회만${C.reset} 실행
195
195
  예: ${C.dim}Run a workflow to list files under src/${C.reset}
196
196
  또는: ${C.dim}/deep-research <질문>${C.reset}
197
197
  2. ${C.yellow}그 사이 다른 Task/subagent 작업은 돌리지 마세요${C.reset} (신호 오염 방지)
198
- 3. 워크플로우가 끝나면: ${C.bold}forgen probe-workflow report${C.reset}
198
+ 3. 워크플로우가 끝나면: ${C.bold}forgen dev probe-workflow report${C.reset}
199
199
 
200
200
  ${C.dim}전제: forgen 의 SubagentStart/Stop·PostToolUse 훅이 설치/활성 상태여야 합니다.
201
201
  불확실하면 'forgen config hooks' 로 확인하세요.${C.reset}
@@ -219,7 +219,7 @@ function colorForOutcome(outcome) {
219
219
  function reportProbe() {
220
220
  const baseline = loadBaseline();
221
221
  if (!baseline) {
222
- console.log(`\n ${C.red}✗ armed 상태가 아닙니다.${C.reset} 먼저 ${C.bold}forgen probe-workflow arm${C.reset} 를 실행하세요.\n`);
222
+ console.log(`\n ${C.red}✗ armed 상태가 아닙니다.${C.reset} 먼저 ${C.bold}forgen dev probe-workflow arm${C.reset} 를 실행하세요.\n`);
223
223
  process.exitCode = 1;
224
224
  return;
225
225
  }
@@ -228,7 +228,7 @@ function reportProbe() {
228
228
  persistResult(baseline, verdict);
229
229
  const oc = colorForOutcome(verdict.outcome);
230
230
  console.log(`
231
- ${C.bold}forgen probe-workflow — report${C.reset} ${C.dim}(armed ${baseline.armedIso})${C.reset}
231
+ ${C.bold}forgen dev probe-workflow — report${C.reset} ${C.dim}(armed ${baseline.armedIso})${C.reset}
232
232
 
233
233
  SubagentStart/Stop 발화 : ${verdict.subagentStartStopFired ? `${C.green}YES${C.reset}` : `${C.yellow}NO${C.reset}`}
234
234
  PostToolUse 발화 : ${verdict.postToolUseFired ? `${C.green}YES${C.reset}` : `${C.yellow}NO${C.reset}`}
@@ -254,8 +254,8 @@ function persistResult(baseline, verdict) {
254
254
  function statusProbe() {
255
255
  const baseline = loadBaseline();
256
256
  console.log(baseline
257
- ? `\n armed: ${C.cyan}${baseline.armedIso}${C.reset}\n → 워크플로우 실행 후 ${C.bold}forgen probe-workflow report${C.reset}\n`
258
- : `\n ${C.dim}armed 상태 아님.${C.reset} ${C.bold}forgen probe-workflow arm${C.reset} 로 시작하세요.\n`);
257
+ ? `\n armed: ${C.cyan}${baseline.armedIso}${C.reset}\n → 워크플로우 실행 후 ${C.bold}forgen dev probe-workflow report${C.reset}\n`
258
+ : `\n ${C.dim}armed 상태 아님.${C.reset} ${C.bold}forgen dev probe-workflow arm${C.reset} 로 시작하세요.\n`);
259
259
  }
260
260
  export async function handleProbeWorkflow(args) {
261
261
  const sub = args[0] ?? 'status';
@@ -271,12 +271,12 @@ export async function handleProbeWorkflow(args) {
271
271
  return;
272
272
  default:
273
273
  console.log(`
274
- ${C.bold}forgen probe-workflow${C.reset} — ADR-009 §1: 워크플로우 훅 발화 실측
274
+ ${C.bold}forgen dev probe-workflow${C.reset} — ADR-009 §1: 워크플로우 훅 발화 실측
275
275
 
276
276
  Usage:
277
- forgen probe-workflow arm baseline 기록 + 안내 (먼저)
278
- forgen probe-workflow report 워크플로우 실행 후 신호 수집 → 판정
279
- forgen probe-workflow status 현재 armed 상태 확인
277
+ forgen dev probe-workflow arm baseline 기록 + 안내 (먼저)
278
+ forgen dev probe-workflow report 워크플로우 실행 후 신호 수집 → 판정
279
+ forgen dev probe-workflow status 현재 armed 상태 확인
280
280
  `);
281
281
  }
282
282
  }
@@ -107,7 +107,7 @@ export async function handleRecall(args) {
107
107
  return;
108
108
  }
109
109
  console.log('');
110
- console.log(` forgen recall — last ${recalls.length} surfaced solution${recalls.length === 1 ? '' : 's'}`);
110
+ console.log(` forgen status --compound — last ${recalls.length} surfaced solution${recalls.length === 1 ? '' : 's'}`);
111
111
  console.log(' ─────────────────────────────────────────');
112
112
  for (const r of recalls) {
113
113
  const score = r.match_score !== undefined ? ` @${r.match_score.toFixed(2)}` : '';
@@ -27,7 +27,7 @@ export async function handleRegressMap(args = []) {
27
27
  console.log('regress-map: git unavailable or no commits in window.');
28
28
  return;
29
29
  }
30
- console.log(' ┌─ forgen regress-map ───────────────────────────────────┐');
30
+ console.log(' ┌─ forgen dev regress-map ───────────────────────────────────┐');
31
31
  console.log(` │ window: last ${map.windowDays} days · fix commits: ${map.fixCommits}`.padEnd(60) + '│');
32
32
  if (ratio.available) {
33
33
  console.log(` │ ${formatFixRatio(ratio)}`.padEnd(60) + '│');
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Forgen v0.5.0 — Rendered Rules Manifest (ADR-010 W1-1)
3
+ *
4
+ * harness 가 렌더해 쓴 규칙 파일의 content-hash 원장.
5
+ * reclaimer(`forgen migrate tenetx`)가 "forgen 이 쓴 그대로인 파일"과
6
+ * "사용자가 편집했거나 다른 도구가 쓴 파일"을 구분하는 유일한 근거다 —
7
+ * native /doctor 는 비용만 알지 provenance 를 모른다.
8
+ *
9
+ * 성능 (Rev 2): 렌더 시점에 이미 메모리에 있는 내용을 해시하므로
10
+ * 재읽기/전체 스캔 비용이 없다. 스캔 사이드(reclaimer)만 파일을 읽는다.
11
+ */
12
+ export interface ManifestEntry {
13
+ sha256: string;
14
+ version: string;
15
+ ts: string;
16
+ }
17
+ export type RulesManifest = Record<string, ManifestEntry>;
18
+ export declare function manifestPath(home?: string): string;
19
+ export declare function sha256Of(content: string): string;
20
+ export declare function loadManifest(home?: string): RulesManifest;
21
+ /**
22
+ * 렌더 직후 호출 — 쓴 파일들의 해시를 기존 manifest 에 병합 기록.
23
+ * 같은 경로는 최신 해시로 **덮어쓴다** (history 없음): 같은 경로에 구버전
24
+ * 내용이 남는 시나리오는 렌더가 즉시 갱신하므로 존재하지 않고, 다른 경로의
25
+ * 구버전 파일은 marker-only(needs-confirm) 경로로 안전 강등된다.
26
+ */
27
+ export declare function recordRenderedFiles(files: Record<string, string>, version: string, home?: string): void;
28
+ /** 파일 내용이 manifest 의 어떤 엔트리와도 일치하는가 (경로 키 기준) */
29
+ export declare function matchesManifest(absPath: string, content: string, manifest: RulesManifest): boolean;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Forgen v0.5.0 — Rendered Rules Manifest (ADR-010 W1-1)
3
+ *
4
+ * harness 가 렌더해 쓴 규칙 파일의 content-hash 원장.
5
+ * reclaimer(`forgen migrate tenetx`)가 "forgen 이 쓴 그대로인 파일"과
6
+ * "사용자가 편집했거나 다른 도구가 쓴 파일"을 구분하는 유일한 근거다 —
7
+ * native /doctor 는 비용만 알지 provenance 를 모른다.
8
+ *
9
+ * 성능 (Rev 2): 렌더 시점에 이미 메모리에 있는 내용을 해시하므로
10
+ * 재읽기/전체 스캔 비용이 없다. 스캔 사이드(reclaimer)만 파일을 읽는다.
11
+ */
12
+ import * as fs from 'node:fs';
13
+ import * as os from 'node:os';
14
+ import * as path from 'node:path';
15
+ import { createHash } from 'node:crypto';
16
+ export function manifestPath(home = os.homedir()) {
17
+ return path.join(home, '.forgen', 'state', 'rendered-rules-manifest.json');
18
+ }
19
+ export function sha256Of(content) {
20
+ return createHash('sha256').update(content, 'utf-8').digest('hex');
21
+ }
22
+ export function loadManifest(home = os.homedir()) {
23
+ try {
24
+ const raw = fs.readFileSync(manifestPath(home), 'utf-8');
25
+ const parsed = JSON.parse(raw);
26
+ return typeof parsed === 'object' && parsed !== null ? parsed : {};
27
+ }
28
+ catch {
29
+ return {};
30
+ }
31
+ }
32
+ /**
33
+ * 렌더 직후 호출 — 쓴 파일들의 해시를 기존 manifest 에 병합 기록.
34
+ * 같은 경로는 최신 해시로 **덮어쓴다** (history 없음): 같은 경로에 구버전
35
+ * 내용이 남는 시나리오는 렌더가 즉시 갱신하므로 존재하지 않고, 다른 경로의
36
+ * 구버전 파일은 marker-only(needs-confirm) 경로로 안전 강등된다.
37
+ */
38
+ export function recordRenderedFiles(files, version, home = os.homedir()) {
39
+ try {
40
+ const p = manifestPath(home);
41
+ fs.mkdirSync(path.dirname(p), { recursive: true });
42
+ const manifest = loadManifest(home);
43
+ const ts = new Date().toISOString();
44
+ for (const [absPath, content] of Object.entries(files)) {
45
+ manifest[absPath] = { sha256: sha256Of(content), version, ts };
46
+ }
47
+ fs.writeFileSync(p, `${JSON.stringify(manifest, null, 2)}\n`);
48
+ }
49
+ catch {
50
+ // manifest 기록 실패는 렌더를 막지 않는다 (fail-open) —
51
+ // 그 경우 reclaimer 가 marker-only 경로로 폴백한다.
52
+ }
53
+ }
54
+ /** 파일 내용이 manifest 의 어떤 엔트리와도 일치하는가 (경로 키 기준) */
55
+ export function matchesManifest(absPath, content, manifest) {
56
+ const entry = manifest[absPath];
57
+ if (!entry)
58
+ return false;
59
+ return entry.sha256 === sha256Of(content);
60
+ }
@@ -11,6 +11,7 @@ import * as path from 'node:path';
11
11
  import { createLogger } from './logger.js';
12
12
  import { FORGEN_HOME } from './paths.js';
13
13
  import { ensureObservabilitySchema } from './observability-store.js';
14
+ import { stripPrivate } from '../engine/private-filter.js';
14
15
  const require = createRequire(import.meta.url);
15
16
  // Suppress ExperimentalWarning for node:sqlite (Node.js 22+)
16
17
  {
@@ -116,8 +117,13 @@ export async function indexCodexSession(cwd, transcriptPath, sessionId) {
116
117
  const role = entry.payload.role;
117
118
  if (role !== 'user' && role !== 'assistant')
118
119
  continue;
119
- const text = extractCodexText(entry.payload.content);
120
- if (!text)
120
+ const rawText = extractCodexText(entry.payload.content);
121
+ if (!rawText)
122
+ continue;
123
+ // W2-5 (private 태그): FTS 인덱스는 session-search 로 재노출되므로 <private>
124
+ // 범위를 인덱싱 전에 제거한다. 통째 private 메시지는 인덱싱하지 않는다.
125
+ const text = stripPrivate(rawText).cleaned;
126
+ if (!text.trim())
121
127
  continue;
122
128
  const truncated = text.slice(0, 10000);
123
129
  const ts = typeof entry.timestamp === 'string' ? entry.timestamp : '';
@@ -179,7 +185,12 @@ export async function indexSession(cwd, transcriptPath, sessionId) {
179
185
  : '';
180
186
  }
181
187
  if (role && text) {
182
- const truncated = text.slice(0, 10000);
188
+ // W2-5 (private 태그): FTS 인덱스가 session-search 로 재노출되므로 <private>
189
+ // 범위를 제거 후 인덱싱. 통째 private 메시지는 스킵.
190
+ const cleanText = stripPrivate(text).cleaned;
191
+ if (!cleanText.trim())
192
+ continue;
193
+ const truncated = cleanText.slice(0, 10000);
183
194
  const result = insertMsg.run(sessionId, role, truncated, entry.timestamp ?? '');
184
195
  // FTS5 인덱스 동기화
185
196
  if (fts5Available) {
@@ -6,6 +6,9 @@
6
6
  */
7
7
  import type { RuntimeHost } from './types.js';
8
8
  import type { V1BootstrapResult } from './v1-bootstrap.js';
9
+ export declare function stripForgenManagedRules(rules: string[]): string[];
10
+ /** Apply v1 trust policy → permissions (deny/ask lists). */
11
+ export declare function applyTrustPolicyPermissions(settings: Record<string, unknown>, v1Result: V1BootstrapResult): void;
9
12
  /**
10
13
  * Inject forgen settings into Claude Code settings.json.
11
14
  * Coordinates: read/backup → env merge → statusLine → hooks → trust policy → atomic write.
@@ -12,13 +12,20 @@ import { ConfigError } from './errors.js';
12
12
  import { createLogger } from './logger.js';
13
13
  import { acquireLock, atomicWriteFileSync, CLAUDE_DIR, readSettingsSafely, releaseLock, rollbackSettings, SETTINGS_BACKUP_PATH, SETTINGS_PATH, } from './settings-lock.js';
14
14
  const log = createLogger('settings-injector');
15
+ // 주의(2026-08-04 버그수정): '# forgen-managed' 는 **더 이상 주입하지 않는다** — JSON
16
+ // permissions 배열엔 주석이 데이터 원소가 되어 Claude Code 가 malformed deny 룰로 경고했다.
17
+ // strip-set 에는 남겨둬, 이전 버그로 오염된 사용자 settings 가 재주입 시 self-heal 되게 한다.
18
+ // 멱등성은 마커가 아니라 아래 실제 룰 문자열의 정확일치 제거로 이미 보장된다.
15
19
  const FORGEN_PERMISSION_RULES = new Set([
16
- '# forgen-managed',
20
+ '# forgen-managed', // legacy 오염 정리용 (신규 주입 안 함)
17
21
  'Bash(rm -rf *)',
18
22
  'Bash(git push --force*)',
19
23
  'Bash(git reset --hard*)',
20
24
  ]);
21
- function stripForgenManagedRules(rules) {
25
+ /** 신규 주입할 forgen deny 룰 (마커 없음). */
26
+ const FORGEN_DENY_RULES = ['Bash(rm -rf *)', 'Bash(git push --force*)', 'Bash(git reset --hard*)'];
27
+ const FORGEN_ASK_RULES = ['Bash(rm -rf *)', 'Bash(git push --force*)'];
28
+ export function stripForgenManagedRules(rules) {
22
29
  return rules.filter((r) => !FORGEN_PERMISSION_RULES.has(r));
23
30
  }
24
31
  /**
@@ -116,29 +123,18 @@ function mergeHooksIntoSettings(settings, runtime, cwd, pkgRoot) {
116
123
  }
117
124
  }
118
125
  /** Apply v1 trust policy → permissions (deny/ask lists). */
119
- function applyTrustPolicyPermissions(settings, v1Result) {
126
+ export function applyTrustPolicyPermissions(settings, v1Result) {
120
127
  if (!v1Result.session)
121
128
  return;
122
129
  const trust = v1Result.session.effective_trust_policy;
123
130
  const permissions = settings.permissions ?? {};
124
131
  const existingDeny = stripForgenManagedRules(permissions.deny ?? []);
125
132
  if (trust === '가드레일 우선') {
126
- permissions.deny = [
127
- ...existingDeny,
128
- '# forgen-managed',
129
- 'Bash(rm -rf *)',
130
- 'Bash(git push --force*)',
131
- 'Bash(git reset --hard*)',
132
- ];
133
+ permissions.deny = [...existingDeny, ...FORGEN_DENY_RULES];
133
134
  }
134
135
  else if (trust === '승인 완화') {
135
136
  const existingAsk = stripForgenManagedRules(permissions.ask ?? []);
136
- permissions.ask = [
137
- ...existingAsk,
138
- '# forgen-managed',
139
- 'Bash(rm -rf *)',
140
- 'Bash(git push --force*)',
141
- ];
137
+ permissions.ask = [...existingAsk, ...FORGEN_ASK_RULES];
142
138
  permissions.deny = existingDeny.length > 0 ? existingDeny : undefined;
143
139
  }
144
140
  // '완전 신뢰 실행': 추가 제한 없음
@@ -1,5 +1,23 @@
1
1
  import type { V1HarnessContext } from './harness.js';
2
2
  import type { RuntimeHost } from './types.js';
3
+ /**
4
+ * 사용자 메시지 수 카운트 (streaming).
5
+ *
6
+ * Audit fix #8 (2026-04-21): 이전에는 `fs.readFileSync(transcript, 'utf-8')`로
7
+ * 파일 전체를 메모리에 올렸다. 수백 MB 규모 transcript에서는 heap spike가
8
+ * 발생했고, 카운트 외엔 내용이 필요 없으니 streaming line-by-line로 충분하다.
9
+ */
10
+ /**
11
+ * 0.4.6 — claude/codex 양 schema 호환.
12
+ *
13
+ * Claude JSONL: {type: 'user' | 'queue-operation', ...}
14
+ * Codex JSONL: {type: 'response_item', payload: {role: 'user' | 'developer' | 'assistant', ...}}
15
+ *
16
+ * 단일 함수에서 둘 다 처리 — schema 자동 감지.
17
+ */
18
+ export declare function countUserMessages(transcriptPath: string): Promise<number>;
19
+ /** runAutoCompound 결과 — sweep(무인 cron)이 실제 spawn 만 기록하도록 상태 반환. */
20
+ export type AutoCompoundStatus = 'spawned' | 'skipped-inflight' | 'skipped-dedup' | 'failed';
3
21
  /**
4
22
  * 세션 종료 후 자동 compound 추출을 백그라운드(detached)로 시작.
5
23
  *
@@ -12,7 +30,25 @@ import type { RuntimeHost } from './types.js';
12
30
  * dedup: last-auto-compound.json 을 Stop 훅과 공유 — 같은 세션의 최근 run 이 있으면
13
31
  * skip 하여 detached spawn 의 double-run 을 방지한다.
14
32
  */
15
- export declare function runAutoCompound(cwd: string, transcriptPath: string, sessionId: string): void;
33
+ /**
34
+ * per-session in-flight 슬롯 확보 (spawn *전* 표식). 이미 in-flight(TTL 내)면 false 반환 → skip.
35
+ * runAutoCompound 와 context-guard.maybeSpawnAutoCompound 가 **공유**해 이중 spawn(중복 Haiku
36
+ * 과금)을 막는다 (리뷰 SEV-2 #3: 완료-게이트 단일슬롯만으로는 부족).
37
+ */
38
+ export declare function claimAutoCompoundInflight(sessionId: string, now?: number): boolean;
39
+ /** in-flight 슬롯 해제 (spawn 실패 시 재시도 가능하도록). */
40
+ export declare function releaseAutoCompoundInflight(sessionId: string): void;
41
+ /**
42
+ * ~/.forgen/state/auto-compound/<sessionId>.log — 세션별 러너 로그 경로.
43
+ *
44
+ * 결함2 관측성 fix (2026-08-18): 이전엔 detached spawn 을 `stdio: 'ignore'` 로
45
+ * 실행해 러너의 stdout/stderr(진단 메시지 포함)를 통째로 버렸다. 그 결과 solution
46
+ * extraction 이 98/98 sweep 에서 `extractedSolutions=0` 을 기록해도 원인을 알 방법이
47
+ * 없었다(완전 무음 실패). 이제 fd 를 파일로 열어 넘겨 러너의 모든 출력을 세션별
48
+ * 로그로 남긴다 — 다음 실패 모드를 디버그 가능하게 만드는 전제 조건.
49
+ */
50
+ export declare function autoCompoundLogPath(sessionId: string): string;
51
+ export declare function runAutoCompound(cwd: string, transcriptPath: string, sessionId: string, promptCount?: number): AutoCompoundStatus;
16
52
  /**
17
53
  * Plan B-1: 세션 transcript 사후 스캔으로 rate-limit 감지.
18
54
  *
@@ -112,7 +112,7 @@ function findSessionTranscript(cwd, sessionStartMs, preSnapshot, runtime = 'clau
112
112
  *
113
113
  * 단일 함수에서 둘 다 처리 — schema 자동 감지.
114
114
  */
115
- async function countUserMessages(transcriptPath) {
115
+ export async function countUserMessages(transcriptPath) {
116
116
  const { createInterface } = await import('node:readline');
117
117
  const stream = fs.createReadStream(transcriptPath, { encoding: 'utf-8' });
118
118
  const rl = createInterface({ input: stream, crlfDelay: Infinity });
@@ -225,6 +225,34 @@ async function scanCommitDiffForActedOn(sessionId, cwd, sessionStartTime) {
225
225
  */
226
226
  // Stop 훅(maybeSpawnAutoCompound)/session-recovery 와 공유하는 dedup cooldown.
227
227
  const AUTO_COMPOUND_DEDUP_COOLDOWN_MS = 5 * 60 * 1000; // 5min
228
+ // per-session in-flight start-gate TTL — 러너 최대 실행(~210s)+마진. ADR-011 리뷰 SEV-1:
229
+ // 완료-게이트 단일슬롯 마커는 sweep↔Stop 훅 / 다중세션 double-spawn 을 못 막는다.
230
+ // 세션별 start-gate 로 spawn 전에 표식해 이중실행(중복 Haiku 과금)을 차단.
231
+ const AUTO_COMPOUND_INFLIGHT_TTL_MS = 10 * 60 * 1000; // 10min
232
+ /** 세션별 in-flight 마커 경로 (파일명 안전화). */
233
+ function inflightPathFor(sessionId) {
234
+ return path.join(STATE_DIR, 'compound-inflight', `${sessionId.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`);
235
+ }
236
+ /** TTL 지난 in-flight 마커 정리 (무한 증가 방지). */
237
+ function cleanupStaleInflight(dir, now) {
238
+ try {
239
+ for (const f of fs.readdirSync(dir)) {
240
+ const fp = path.join(dir, f);
241
+ try {
242
+ const { startedAt } = JSON.parse(fs.readFileSync(fp, 'utf-8'));
243
+ if (!Number.isFinite(startedAt) || now - startedAt >= AUTO_COMPOUND_INFLIGHT_TTL_MS) {
244
+ fs.rmSync(fp, { force: true });
245
+ }
246
+ }
247
+ catch {
248
+ fs.rmSync(fp, { force: true }); // 손상 마커 제거
249
+ }
250
+ }
251
+ }
252
+ catch {
253
+ /* dir 없음 — noop */
254
+ }
255
+ }
228
256
  /**
229
257
  * 세션 종료 후 자동 compound 추출을 백그라운드(detached)로 시작.
230
258
  *
@@ -237,33 +265,112 @@ const AUTO_COMPOUND_DEDUP_COOLDOWN_MS = 5 * 60 * 1000; // 5min
237
265
  * dedup: last-auto-compound.json 을 Stop 훅과 공유 — 같은 세션의 최근 run 이 있으면
238
266
  * skip 하여 detached spawn 의 double-run 을 방지한다.
239
267
  */
240
- export function runAutoCompound(cwd, transcriptPath, sessionId) {
241
- const markerPath = path.join(STATE_DIR, 'last-auto-compound.json');
268
+ /**
269
+ * per-session in-flight 슬롯 확보 (spawn *전* 표식). 이미 in-flight(TTL 내)면 false 반환 → skip.
270
+ * runAutoCompound 와 context-guard.maybeSpawnAutoCompound 가 **공유**해 이중 spawn(중복 Haiku
271
+ * 과금)을 막는다 (리뷰 SEV-2 #3: 완료-게이트 단일슬롯만으로는 부족).
272
+ */
273
+ export function claimAutoCompoundInflight(sessionId, now = Date.now()) {
274
+ const inflightPath = inflightPathFor(sessionId);
275
+ try {
276
+ const { startedAt } = JSON.parse(fs.readFileSync(inflightPath, 'utf-8'));
277
+ if (Number.isFinite(startedAt) && now - startedAt < AUTO_COMPOUND_INFLIGHT_TTL_MS)
278
+ return false;
279
+ }
280
+ catch {
281
+ /* 마커 없음/손상 — 진행 */
282
+ }
283
+ const dir = path.dirname(inflightPath);
284
+ cleanupStaleInflight(dir, now);
242
285
  try {
243
- const parsed = JSON.parse(fs.readFileSync(markerPath, 'utf-8'));
286
+ fs.mkdirSync(dir, { recursive: true });
287
+ fs.writeFileSync(inflightPath, JSON.stringify({ startedAt: now, sessionId }));
288
+ }
289
+ catch (e) {
290
+ log.debug('in-flight 마커 기록 실패 (계속)', e);
291
+ }
292
+ return true;
293
+ }
294
+ /** in-flight 슬롯 해제 (spawn 실패 시 재시도 가능하도록). */
295
+ export function releaseAutoCompoundInflight(sessionId) {
296
+ try {
297
+ fs.rmSync(inflightPathFor(sessionId), { force: true });
298
+ }
299
+ catch {
300
+ /* noop */
301
+ }
302
+ }
303
+ /**
304
+ * ~/.forgen/state/auto-compound/<sessionId>.log — 세션별 러너 로그 경로.
305
+ *
306
+ * 결함2 관측성 fix (2026-08-18): 이전엔 detached spawn 을 `stdio: 'ignore'` 로
307
+ * 실행해 러너의 stdout/stderr(진단 메시지 포함)를 통째로 버렸다. 그 결과 solution
308
+ * extraction 이 98/98 sweep 에서 `extractedSolutions=0` 을 기록해도 원인을 알 방법이
309
+ * 없었다(완전 무음 실패). 이제 fd 를 파일로 열어 넘겨 러너의 모든 출력을 세션별
310
+ * 로그로 남긴다 — 다음 실패 모드를 디버그 가능하게 만드는 전제 조건.
311
+ */
312
+ export function autoCompoundLogPath(sessionId) {
313
+ return path.join(STATE_DIR, 'auto-compound', `${sessionId.replace(/[^a-zA-Z0-9_-]/g, '_')}.log`);
314
+ }
315
+ export function runAutoCompound(cwd, transcriptPath, sessionId, promptCount = 0) {
316
+ const now = Date.now();
317
+ // 1. 완료 마커 cooldown (secondary, 단일슬롯 — 값싼 read 먼저).
318
+ try {
319
+ const parsed = JSON.parse(fs.readFileSync(path.join(STATE_DIR, 'last-auto-compound.json'), 'utf-8'));
244
320
  if (parsed.sessionId === sessionId) {
245
321
  const last = parsed.completedAt ? Date.parse(parsed.completedAt) : 0;
246
- if (Number.isFinite(last) && Date.now() - last < AUTO_COMPOUND_DEDUP_COOLDOWN_MS) {
322
+ if (Number.isFinite(last) && now - last < AUTO_COMPOUND_DEDUP_COOLDOWN_MS) {
247
323
  log.debug('auto-compound skip: 최근 동일 세션 run 존재 (dedup)');
248
- return;
324
+ return 'skipped-dedup';
249
325
  }
250
326
  }
251
327
  }
252
328
  catch {
253
- // 마커 없음(최초)/손상 — 진행 (fail-open).
329
+ /* 마커 없음(최초)/손상 — 진행 (fail-open). */
330
+ }
331
+ // 2. per-session in-flight start-gate (공유 헬퍼).
332
+ if (!claimAutoCompoundInflight(sessionId, now)) {
333
+ log.debug('auto-compound skip: 세션 in-flight (start-gate)');
334
+ return 'skipped-inflight';
254
335
  }
255
336
  const runnerPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'auto-compound-runner.js');
337
+ // 로그 파일 fd 열기 — 실패(디스크 문제 등)해도 fail-open 으로 'ignore' 폴백.
338
+ const logPath = autoCompoundLogPath(sessionId);
339
+ let logFd;
340
+ try {
341
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
342
+ logFd = fs.openSync(logPath, 'a');
343
+ }
344
+ catch (e) {
345
+ log.debug('auto-compound 로그 파일 열기 실패 — stdio ignore로 폴백', e);
346
+ }
256
347
  try {
257
- const child = spawn('node', [runnerPath, cwd, transcriptPath, sessionId], {
348
+ // 결함2 corrected fix (2026-08-19): 리터럴 'node' 대신 process.execPath 로 spawn.
349
+ // 실 cron 은 절대 nvm node 경로로 sweep 를 돌리는데, 러너를 'node' 로 spawn 하면
350
+ // cron sparse PATH 의 /bin/node 로 해석돼 러너의 process.execPath 가 달라지고,
351
+ // 러너가 forgen(nvm 형제 바이너리)을 절대경로로 못 찾아 solution write 가 ENOENT 로
352
+ // 실패한다. execPath 를 물려주면 러너가 sweep 과 같은 node(=forgen 형제)를 상속한다.
353
+ const child = spawn(process.execPath, [runnerPath, cwd, transcriptPath, sessionId, String(promptCount)], {
258
354
  cwd,
259
355
  detached: true,
260
- stdio: 'ignore',
356
+ stdio: logFd !== undefined ? ['ignore', logFd, logFd] : 'ignore',
261
357
  });
262
358
  child.unref();
359
+ if (logFd !== undefined)
360
+ fs.closeSync(logFd); // child가 dup — 부모 측 fd는 닫아도 안전
263
361
  console.log('\n[forgen] 세션 분석을 백그라운드로 시작했습니다 (자동 compound) — 결과는 다음 세션 시작 시 반영됩니다.\n');
362
+ return 'spawned';
264
363
  }
265
364
  catch (e) {
365
+ if (logFd !== undefined) {
366
+ try {
367
+ fs.closeSync(logFd);
368
+ }
369
+ catch { /* noop */ }
370
+ }
266
371
  log.debug('auto-compound 시작 실패', e);
372
+ releaseAutoCompoundInflight(sessionId); // spawn 실패 시 표식 제거 → 재시도 가능
373
+ return 'failed';
267
374
  }
268
375
  }
269
376
  /**
@@ -31,6 +31,7 @@ const SESSION_SCOPED_PREFIXES = [
31
31
  'reminder-',
32
32
  'context-',
33
33
  'last-',
34
+ 'current-model-', // W4-3 per-model 가드 프로필의 세션별 모델 캐시
34
35
  ];
35
36
  const DEFAULT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
36
37
  function hasSessionPrefix(name) {
@@ -0,0 +1,20 @@
1
+ /**
2
+ * forgen status — 통합 상태 명령 (Wave 1, feature-audit 2026-07-21).
3
+ *
4
+ * 이전에는 "내 상태를 보여줘"가 stats/health/dashboard/me/recall/explain/
5
+ * last-block/watch/inspect 9개 명령으로 파편화돼 있었다(~1800줄). 이 하나로
6
+ * 통합해 표면을 줄이고 진입점을 일원화한다. 기존 render/compute 함수는 재사용만
7
+ * 하고 재작성하지 않는다.
8
+ *
9
+ * forgen status 요약 (health 헤더 + one-screen stats)
10
+ * forgen status --compound compound 상태 + 최근 회상 이력
11
+ * forgen status --profile 4축 프로필 + 최근 교정
12
+ * forgen status --rules 활성 룰
13
+ * forgen status --blocks [N] 최근 차단 N건 (rule·사유·해결)
14
+ * forgen status --live 실시간 훅 이벤트 스트림
15
+ */
16
+ declare const VIEWS: readonly ["--compound", "--profile", "--rules", "--blocks", "--live", "--overview"];
17
+ type View = (typeof VIEWS)[number];
18
+ export declare function resolveView(args: string[]): View | null;
19
+ export declare function handleStatus(args: string[]): Promise<void>;
20
+ export {};
@@ -0,0 +1,100 @@
1
+ /**
2
+ * forgen status — 통합 상태 명령 (Wave 1, feature-audit 2026-07-21).
3
+ *
4
+ * 이전에는 "내 상태를 보여줘"가 stats/health/dashboard/me/recall/explain/
5
+ * last-block/watch/inspect 9개 명령으로 파편화돼 있었다(~1800줄). 이 하나로
6
+ * 통합해 표면을 줄이고 진입점을 일원화한다. 기존 render/compute 함수는 재사용만
7
+ * 하고 재작성하지 않는다.
8
+ *
9
+ * forgen status 요약 (health 헤더 + one-screen stats)
10
+ * forgen status --compound compound 상태 + 최근 회상 이력
11
+ * forgen status --profile 4축 프로필 + 최근 교정
12
+ * forgen status --rules 활성 룰
13
+ * forgen status --blocks [N] 최근 차단 N건 (rule·사유·해결)
14
+ * forgen status --live 실시간 훅 이벤트 스트림
15
+ */
16
+ const VIEWS = ['--compound', '--profile', '--rules', '--blocks', '--live', '--overview'];
17
+ const ALIASES = {
18
+ '-c': '--compound',
19
+ '-p': '--profile',
20
+ '-r': '--rules',
21
+ '-b': '--blocks',
22
+ '-l': '--live',
23
+ '-o': '--overview',
24
+ };
25
+ export function resolveView(args) {
26
+ for (const a of args) {
27
+ const flag = a.split('=')[0]; // --blocks=1 → --blocks
28
+ if (VIEWS.includes(flag))
29
+ return flag;
30
+ if (ALIASES[flag])
31
+ return ALIASES[flag];
32
+ }
33
+ return null;
34
+ }
35
+ export async function handleStatus(args) {
36
+ // 하위호환: 구 `forgen status --watch|--json|--interval N`(observability dashboard)
37
+ // 은 통합 status 의 observability 모드로 계속 지원. 통합 전 별도 status 였음.
38
+ if (args.includes('--watch') || args.includes('--json') || args.includes('--interval')) {
39
+ const { runDashboard } = await import('./dashboard-cli.js');
40
+ const intervalIdx = args.indexOf('--interval');
41
+ await runDashboard({
42
+ watch: args.includes('--watch'),
43
+ json: args.includes('--json'),
44
+ intervalSec: intervalIdx !== -1 ? Number(args[intervalIdx + 1]) || 5 : 5,
45
+ });
46
+ return;
47
+ }
48
+ const view = resolveView(args);
49
+ switch (view) {
50
+ case '--compound': {
51
+ const { runDashboard } = await import('./dashboard-cli.js');
52
+ await runDashboard({});
53
+ const { handleRecall } = await import('./recall-cli.js');
54
+ await handleRecall(args.filter((a) => a !== '--compound' && a !== '-c'));
55
+ return;
56
+ }
57
+ case '--profile': {
58
+ const { handleInspect } = await import('./inspect-cli.js');
59
+ await handleInspect(['profile']);
60
+ await handleInspect(['corrections']);
61
+ return;
62
+ }
63
+ case '--rules': {
64
+ const { handleInspect } = await import('./inspect-cli.js');
65
+ await handleInspect(['rules']);
66
+ return;
67
+ }
68
+ case '--blocks': {
69
+ // 남은 positional(숫자)이 있으면 explain N, 없으면 최근 1건.
70
+ const { handleExplain } = await import('./explain-cli.js');
71
+ await handleExplain(args.filter((a) => a !== '--blocks' && a !== '-b'));
72
+ return;
73
+ }
74
+ case '--live': {
75
+ const { handleWatch } = await import('./watch-cli.js');
76
+ await handleWatch();
77
+ return;
78
+ }
79
+ case '--overview': {
80
+ // 리치 운영 대시보드(hook health·session history·learning curve·multi-host).
81
+ // 구 `forgen dashboard`의 고유 콘텐츠 — --compound(ROI/compound)와 다른 축이라
82
+ // 별도 뷰로 보존 (Wave 1 리뷰: 콘텐츠 손실 방지).
83
+ const { handleDashboard } = await import('./dashboard.js');
84
+ await handleDashboard();
85
+ return;
86
+ }
87
+ default: {
88
+ // 요약: health 헤더 + one-screen stats (둘 다 computeStats() 기반).
89
+ const { computeHealth, renderHealthLine } = await import('./health-cli.js');
90
+ const { computeStats, renderStats } = await import('./stats-cli.js');
91
+ console.log(renderHealthLine(computeHealth()));
92
+ console.log(renderStats(computeStats()));
93
+ console.log(` ${dim('views:')} forgen status --compound | --profile | --rules | --blocks [N] | --live`);
94
+ return;
95
+ }
96
+ }
97
+ }
98
+ function dim(s) {
99
+ return `\x1b[2m${s}\x1b[0m`;
100
+ }