@nac3/forge-cli 0.2.0-alpha.3 → 0.2.0-alpha.30

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 (162) hide show
  1. package/dist/bin/yf.d.ts.map +1 -1
  2. package/dist/bin/yf.js +27 -0
  3. package/dist/bin/yf.js.map +1 -1
  4. package/dist/chat/claude.d.ts +22 -15
  5. package/dist/chat/claude.d.ts.map +1 -1
  6. package/dist/chat/claude.js +75 -22
  7. package/dist/chat/claude.js.map +1 -1
  8. package/dist/chat/panel.d.ts.map +1 -1
  9. package/dist/chat/panel.js +692 -17
  10. package/dist/chat/panel.js.map +1 -1
  11. package/dist/chat/server.js +630 -32
  12. package/dist/chat/server.js.map +1 -1
  13. package/dist/chat/spec_extract.d.ts.map +1 -1
  14. package/dist/chat/spec_extract.js +39 -0
  15. package/dist/chat/spec_extract.js.map +1 -1
  16. package/dist/chat/tools/audit_consumers.d.ts +66 -0
  17. package/dist/chat/tools/audit_consumers.d.ts.map +1 -0
  18. package/dist/chat/tools/audit_consumers.js +231 -0
  19. package/dist/chat/tools/audit_consumers.js.map +1 -0
  20. package/dist/chat/tools/git.js +4 -4
  21. package/dist/chat/tools/github.js +3 -3
  22. package/dist/chat/tools/lifecycle.js +3 -3
  23. package/dist/chat/tools/manual.js +1 -1
  24. package/dist/chat/tools/reader.js +8 -8
  25. package/dist/chat/tools/workflow.d.ts +45 -0
  26. package/dist/chat/tools/workflow.d.ts.map +1 -0
  27. package/dist/chat/tools/workflow.js +404 -0
  28. package/dist/chat/tools/workflow.js.map +1 -0
  29. package/dist/chat/tools.d.ts.map +1 -1
  30. package/dist/chat/tools.js +23 -4
  31. package/dist/chat/tools.js.map +1 -1
  32. package/dist/commands/approve.d.ts +32 -0
  33. package/dist/commands/approve.d.ts.map +1 -0
  34. package/dist/commands/approve.js +198 -0
  35. package/dist/commands/approve.js.map +1 -0
  36. package/dist/commands/block.d.ts +28 -0
  37. package/dist/commands/block.d.ts.map +1 -0
  38. package/dist/commands/block.js +189 -0
  39. package/dist/commands/block.js.map +1 -0
  40. package/dist/commands/bootstrap.d.ts +35 -0
  41. package/dist/commands/bootstrap.d.ts.map +1 -0
  42. package/dist/commands/bootstrap.js +205 -0
  43. package/dist/commands/bootstrap.js.map +1 -0
  44. package/dist/commands/chat.d.ts +3 -0
  45. package/dist/commands/chat.d.ts.map +1 -1
  46. package/dist/commands/chat.js +46 -1
  47. package/dist/commands/chat.js.map +1 -1
  48. package/dist/commands/clarify.d.ts +30 -0
  49. package/dist/commands/clarify.d.ts.map +1 -0
  50. package/dist/commands/clarify.js +671 -0
  51. package/dist/commands/clarify.js.map +1 -0
  52. package/dist/commands/discover.d.ts +30 -0
  53. package/dist/commands/discover.d.ts.map +1 -0
  54. package/dist/commands/discover.js +178 -0
  55. package/dist/commands/discover.js.map +1 -0
  56. package/dist/commands/doctor.js +94 -42
  57. package/dist/commands/doctor.js.map +1 -1
  58. package/dist/commands/keys_setup.d.ts +53 -0
  59. package/dist/commands/keys_setup.d.ts.map +1 -0
  60. package/dist/commands/keys_setup.js +487 -0
  61. package/dist/commands/keys_setup.js.map +1 -0
  62. package/dist/commands/legacy-audit.d.ts +34 -0
  63. package/dist/commands/legacy-audit.d.ts.map +1 -0
  64. package/dist/commands/legacy-audit.js +270 -0
  65. package/dist/commands/legacy-audit.js.map +1 -0
  66. package/dist/commands/license.d.ts.map +1 -1
  67. package/dist/commands/license.js +41 -0
  68. package/dist/commands/license.js.map +1 -1
  69. package/dist/commands/operate.d.ts +22 -0
  70. package/dist/commands/operate.d.ts.map +1 -0
  71. package/dist/commands/operate.js +523 -0
  72. package/dist/commands/operate.js.map +1 -0
  73. package/dist/commands/spec.d.ts +38 -0
  74. package/dist/commands/spec.d.ts.map +1 -0
  75. package/dist/commands/spec.js +256 -0
  76. package/dist/commands/spec.js.map +1 -0
  77. package/dist/commands/support.d.ts +22 -0
  78. package/dist/commands/support.d.ts.map +1 -0
  79. package/dist/commands/support.js +143 -0
  80. package/dist/commands/support.js.map +1 -0
  81. package/dist/commands/triage.d.ts +34 -0
  82. package/dist/commands/triage.d.ts.map +1 -0
  83. package/dist/commands/triage.js +228 -0
  84. package/dist/commands/triage.js.map +1 -0
  85. package/dist/commands/vault-inventory.d.ts +30 -0
  86. package/dist/commands/vault-inventory.d.ts.map +1 -0
  87. package/dist/commands/vault-inventory.js +214 -0
  88. package/dist/commands/vault-inventory.js.map +1 -0
  89. package/dist/commands/vault.d.ts.map +1 -1
  90. package/dist/commands/vault.js +5 -0
  91. package/dist/commands/vault.js.map +1 -1
  92. package/dist/commands/voice.js +1 -1
  93. package/dist/commands/voice.js.map +1 -1
  94. package/dist/commands/workflow-coverage.d.ts +30 -0
  95. package/dist/commands/workflow-coverage.d.ts.map +1 -0
  96. package/dist/commands/workflow-coverage.js +138 -0
  97. package/dist/commands/workflow-coverage.js.map +1 -0
  98. package/dist/core/keys_envelope.d.ts +13 -0
  99. package/dist/core/keys_envelope.d.ts.map +1 -1
  100. package/dist/core/keys_envelope.js.map +1 -1
  101. package/dist/deploy/adapter.d.ts +93 -0
  102. package/dist/deploy/adapter.d.ts.map +1 -0
  103. package/dist/deploy/adapter.js +42 -0
  104. package/dist/deploy/adapter.js.map +1 -0
  105. package/dist/deploy/aws_adapter.d.ts +28 -0
  106. package/dist/deploy/aws_adapter.d.ts.map +1 -0
  107. package/dist/deploy/aws_adapter.js +98 -0
  108. package/dist/deploy/aws_adapter.js.map +1 -0
  109. package/dist/deploy/cloudflare.d.ts +24 -0
  110. package/dist/deploy/cloudflare.d.ts.map +1 -0
  111. package/dist/deploy/cloudflare.js +169 -0
  112. package/dist/deploy/cloudflare.js.map +1 -0
  113. package/dist/license/hito4_client.d.ts +17 -1
  114. package/dist/license/hito4_client.d.ts.map +1 -1
  115. package/dist/license/hito4_client.js +71 -10
  116. package/dist/license/hito4_client.js.map +1 -1
  117. package/dist/license/index.d.ts.map +1 -1
  118. package/dist/license/index.js +7 -0
  119. package/dist/license/index.js.map +1 -1
  120. package/dist/license/sync.d.ts +54 -0
  121. package/dist/license/sync.d.ts.map +1 -0
  122. package/dist/license/sync.js +131 -0
  123. package/dist/license/sync.js.map +1 -0
  124. package/dist/support/reports.d.ts +31 -0
  125. package/dist/support/reports.d.ts.map +1 -0
  126. package/dist/support/reports.js +162 -0
  127. package/dist/support/reports.js.map +1 -0
  128. package/dist/telemetry/usage.d.ts +67 -0
  129. package/dist/telemetry/usage.d.ts.map +1 -0
  130. package/dist/telemetry/usage.js +208 -0
  131. package/dist/telemetry/usage.js.map +1 -0
  132. package/dist/version.d.ts +1 -1
  133. package/dist/version.d.ts.map +1 -1
  134. package/dist/version.js +1 -1
  135. package/dist/version.js.map +1 -1
  136. package/dist/voice/intents.d.ts +1 -1
  137. package/dist/voice/intents.js +0 -0
  138. package/dist/voice/providers/google.d.ts +9 -0
  139. package/dist/voice/providers/google.d.ts.map +1 -1
  140. package/dist/voice/providers/google.js +204 -28
  141. package/dist/voice/providers/google.js.map +1 -1
  142. package/dist/voice/router.d.ts +10 -0
  143. package/dist/voice/router.d.ts.map +1 -1
  144. package/dist/voice/router.js +39 -20
  145. package/dist/voice/router.js.map +1 -1
  146. package/dist/voice/types.d.ts +5 -2
  147. package/dist/voice/types.d.ts.map +1 -1
  148. package/dist/voice/types.js.map +1 -1
  149. package/dist/workflow/state.d.ts +190 -0
  150. package/dist/workflow/state.d.ts.map +1 -0
  151. package/dist/workflow/state.js +119 -0
  152. package/dist/workflow/state.js.map +1 -0
  153. package/package.json +13 -15
  154. package/templates/nextjs-app/README.md +48 -0
  155. package/templates/nextjs-app/next.config.js +8 -0
  156. package/templates/nextjs-app/package.json +33 -0
  157. package/templates/nextjs-app/src/app/globals.css +43 -0
  158. package/templates/nextjs-app/src/app/layout.tsx +29 -0
  159. package/templates/nextjs-app/src/app/page.tsx +63 -0
  160. package/templates/nextjs-app/src/nac/manifest.ts +36 -0
  161. package/templates/nextjs-app/tsconfig.json +21 -0
  162. package/templates/nextjs-app/yujin.forge.json +11 -0
@@ -248,6 +248,63 @@ async function route(req, res, ctx) {
248
248
  await handleForgeToolDispatch(req, res, ctx);
249
249
  return;
250
250
  }
251
+ /* Open-in-editor escape hatch (Layer A.2). The panel surfaces
252
+ file paths as clickable; click triggers this endpoint, which
253
+ spawns the user's default editor (code / cursor / subl /
254
+ idea, in order). Best-effort -- on failure returns the
255
+ resolved path so the panel can copy-to-clipboard fallback. */
256
+ if (req.method === 'POST' && url.pathname === '/api/forge/open-in-editor') {
257
+ await handleOpenInEditor(req, res, ctx);
258
+ return;
259
+ }
260
+ /* Trust gradient tracking (Layer A.2). Panel increments
261
+ counters when the user expands a diff / opens a file in
262
+ their editor / overrides Forge. Forge reads them at session
263
+ boot and decides whether to surface or hide the detail
264
+ links. */
265
+ if (req.method === 'POST' && url.pathname === '/api/forge/trust-event') {
266
+ await handleTrustEvent(req, res, ctx);
267
+ return;
268
+ }
269
+ if (req.method === 'GET' && url.pathname === '/api/forge/trust-state') {
270
+ await handleTrustState(req, res, ctx);
271
+ return;
272
+ }
273
+ /* alpha.29 -- keys status for the settings dropdown. Returns
274
+ which BYOK provider keys + license are configured, without
275
+ exposing the values themselves. */
276
+ if (req.method === 'GET' && url.pathname === '/api/forge/keys-status') {
277
+ await handleKeysStatus(req, res);
278
+ return;
279
+ }
280
+ /* Favicon -- minimal SVG so Chrome stops 404-ing. The browser
281
+ auto-requests /favicon.ico on every page load; without this,
282
+ each open of the panel logs an error. */
283
+ if (req.method === 'GET' && (url.pathname === '/favicon.ico'
284
+ || url.pathname === '/favicon.svg')) {
285
+ const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">'
286
+ + '<circle cx="16" cy="16" r="14" fill="#1a1a1a"/>'
287
+ + '<path d="M8 22 L16 8 L24 22 M11 17 L21 17" stroke="#fafafa" stroke-width="2" fill="none" stroke-linecap="round"/>'
288
+ + '</svg>';
289
+ res.writeHead(200, {
290
+ 'content-type': 'image/svg+xml',
291
+ 'cache-control': 'public, max-age=86400',
292
+ });
293
+ res.end(svg);
294
+ return;
295
+ }
296
+ /* Support report ingest (2026-05-31 -- F30-style). Panel sends
297
+ * runtime errors (window.onerror, fetch failures, unhandled
298
+ * promise rejections) here so Forge sees what the user saw
299
+ * without them having to copy logs. PII is scrubbed on save. */
300
+ if (req.method === 'POST' && url.pathname === '/api/forge/support/report') {
301
+ await handleSupportReport(req, res);
302
+ return;
303
+ }
304
+ if (req.method === 'GET' && url.pathname === '/api/forge/support/reports') {
305
+ await handleSupportReportList(req, res);
306
+ return;
307
+ }
251
308
  /* Spec doc ingest (V1.37 + V1.38 -- bloque 4.5 scaffolding).
252
309
  Client uploads a spec file (PDF / DOCX / HTML / md / ...)
253
310
  and the server parses it via the existing reader pipeline,
@@ -423,6 +480,19 @@ async function handleVoiceStt(req, res, ctx) {
423
480
  sendJson(res, 400, { ok: false, error: 'audio body is empty' });
424
481
  return;
425
482
  }
483
+ /* 2026-05-31 fix: Google STT sync rejects audio > 1 min with
484
+ HTTP 400. Refuse early with a useful message instead of
485
+ leaking the Google error verbatim. ~4 MB is the practical
486
+ ceiling for 60s of WebM/Opus at 32 kbps. */
487
+ if (audio.length > 4 * 1024 * 1024) {
488
+ sendJson(res, 413, {
489
+ ok: false,
490
+ error: 'audio too long for sync STT (limit ~1 min). '
491
+ + 'Got ' + audio.length + ' bytes. The chat panel auto-stops at 55s; '
492
+ + 'if you hit this from a different client, split the audio.',
493
+ });
494
+ return;
495
+ }
426
496
  /* Optional active document hint (V1.31). Lets the voice intent
427
497
  matcher resolve commands like "siguiente" or "buscar X" against
428
498
  the currently-open reader session. Client sets this header to
@@ -466,17 +536,17 @@ async function handleVoiceStt(req, res, ctx) {
466
536
  * runs.
467
537
  */
468
538
  const FORGE_TOOL_DIRECT_ALLOWLIST = new Set([
469
- 'forge.reader.open',
470
- 'forge.reader.list_documents',
471
- 'forge.reader.read_section',
472
- 'forge.reader.next_block',
473
- 'forge.reader.search',
474
- 'forge.reader.bookmark_set',
475
- 'forge.reader.bookmark_jump',
476
- 'forge.reader.recap',
539
+ 'forge_reader_open',
540
+ 'forge_reader_list_documents',
541
+ 'forge_reader_read_section',
542
+ 'forge_reader_next_block',
543
+ 'forge_reader_search',
544
+ 'forge_reader_bookmark_set',
545
+ 'forge_reader_bookmark_jump',
546
+ 'forge_reader_recap',
477
547
  /* Fase F.8 -- HTML user manuals in 10 languages. Read-only,
478
548
  same safety profile as the reader tools. */
479
- 'forge.manual.open',
549
+ 'forge_manual_open',
480
550
  ]);
481
551
  async function handleForgeToolDispatch(req, res, ctx) {
482
552
  let body;
@@ -948,6 +1018,262 @@ async function handleVoiceTts(req, res, ctx) {
948
1018
  });
949
1019
  }
950
1020
  }
1021
+ /* ============================================================
1022
+ * Layer A.2 -- minority surface (open in editor + trust gradient)
1023
+ * ============================================================ */
1024
+ /** Check that `target` is inside `root` (after resolving both).
1025
+ * Refuses path traversal like ../../../etc/passwd. */
1026
+ function isInside(target, root) {
1027
+ const targetAbs = path.resolve(target);
1028
+ const rootAbs = path.resolve(root) + path.sep;
1029
+ return targetAbs === path.resolve(root) || targetAbs.startsWith(rootAbs);
1030
+ }
1031
+ /** Editor commands tried in order. First match wins. The args
1032
+ * templates are joined with the file path at runtime. All
1033
+ * commands run with stdio ignored + detached so the panel does
1034
+ * not block on the editor's window lifetime. */
1035
+ const EDITOR_CANDIDATES = [
1036
+ { cmd: 'code', args: (f) => [f] }, // VS Code
1037
+ { cmd: 'cursor', args: (f) => [f] }, // Cursor
1038
+ { cmd: 'subl', args: (f) => [f] }, // Sublime Text
1039
+ { cmd: 'idea', args: (f) => [f] }, // IntelliJ family
1040
+ { cmd: 'webstorm', args: (f) => [f] },
1041
+ { cmd: 'nano', args: (f) => [f] },
1042
+ { cmd: 'vim', args: (f) => [f] },
1043
+ ];
1044
+ async function handleOpenInEditor(req, res, ctx) {
1045
+ let body;
1046
+ try {
1047
+ body = JSON.parse(await readBody(req));
1048
+ }
1049
+ catch {
1050
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
1051
+ return;
1052
+ }
1053
+ if (typeof body.path !== 'string' || body.path.trim() === '') {
1054
+ sendJson(res, 400, { ok: false, error: 'path (non-empty string) required' });
1055
+ return;
1056
+ }
1057
+ /* Resolve to absolute under the project root. Refuse paths
1058
+ * that escape the project tree (no user opening /etc/passwd
1059
+ * through the chat). */
1060
+ const requestedRel = body.path.trim().replace(/^[/\\]+/, '');
1061
+ const absolute = path.join(ctx.projectRoot, requestedRel);
1062
+ if (!isInside(absolute, ctx.projectRoot)) {
1063
+ sendJson(res, 400, { ok: false, error: 'path escapes project root' });
1064
+ return;
1065
+ }
1066
+ /* Walk the candidates, spawn the first one that does not
1067
+ * throw ENOENT. */
1068
+ const { spawn } = await import('node:child_process');
1069
+ for (const candidate of EDITOR_CANDIDATES) {
1070
+ try {
1071
+ const cmd = process.platform === 'win32' ? `${candidate.cmd}.cmd` : candidate.cmd;
1072
+ const child = spawn(cmd, candidate.args(absolute), {
1073
+ stdio: 'ignore',
1074
+ detached: true,
1075
+ shell: false,
1076
+ });
1077
+ child.on('error', () => { });
1078
+ child.unref();
1079
+ /* If spawn returned, the binary is at least present.
1080
+ * We do not wait for editor to launch (it might be slow
1081
+ * or might be a background daemon). Report success. */
1082
+ sendJson(res, 200, {
1083
+ ok: true,
1084
+ editor: candidate.cmd,
1085
+ path: absolute,
1086
+ });
1087
+ return;
1088
+ }
1089
+ catch { /* ENOENT etc -- try next candidate */ }
1090
+ }
1091
+ /* No editor found. Return the absolute path so the panel can
1092
+ * offer copy-to-clipboard fallback. */
1093
+ sendJson(res, 200, {
1094
+ ok: false,
1095
+ error: 'no supported editor found in PATH',
1096
+ path: absolute,
1097
+ candidates: EDITOR_CANDIDATES.map((c) => c.cmd),
1098
+ hint: 'Install VS Code (code), Cursor, Sublime (subl), or IntelliJ; or copy the path manually.',
1099
+ });
1100
+ }
1101
+ function trustMetricsPath() {
1102
+ return path.join(configDir(), 'trust_metrics.json');
1103
+ }
1104
+ async function loadTrustMetrics() {
1105
+ const fs = await import('node:fs');
1106
+ try {
1107
+ const raw = await fs.promises.readFile(trustMetricsPath(), 'utf-8');
1108
+ const parsed = JSON.parse(raw);
1109
+ if (parsed && typeof parsed === 'object') {
1110
+ return {
1111
+ v: 1,
1112
+ diff_expansions: Number(parsed.diff_expansions) || 0,
1113
+ editor_opens: Number(parsed.editor_opens) || 0,
1114
+ forge_overrides: Number(parsed.forge_overrides) || 0,
1115
+ last_event_at: typeof parsed.last_event_at === 'string' ? parsed.last_event_at : null,
1116
+ last_proposal_at: typeof parsed.last_proposal_at === 'string' ? parsed.last_proposal_at : null,
1117
+ user_choice: ['show', 'hide', 'default'].includes(parsed.user_choice) ? parsed.user_choice : 'default',
1118
+ };
1119
+ }
1120
+ }
1121
+ catch { /* missing or bad -- return defaults */ }
1122
+ return {
1123
+ v: 1,
1124
+ diff_expansions: 0,
1125
+ editor_opens: 0,
1126
+ forge_overrides: 0,
1127
+ last_event_at: null,
1128
+ last_proposal_at: null,
1129
+ user_choice: 'default',
1130
+ };
1131
+ }
1132
+ async function saveTrustMetrics(m) {
1133
+ const fs = await import('node:fs');
1134
+ await fs.promises.mkdir(configDir(), { recursive: true });
1135
+ await fs.promises.writeFile(trustMetricsPath(), JSON.stringify(m, null, 2), 'utf-8');
1136
+ }
1137
+ async function handleTrustEvent(req, res, _ctx) {
1138
+ let body;
1139
+ try {
1140
+ body = JSON.parse(await readBody(req));
1141
+ }
1142
+ catch {
1143
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
1144
+ return;
1145
+ }
1146
+ const kind = typeof body.kind === 'string' ? body.kind : '';
1147
+ const m = await loadTrustMetrics();
1148
+ const now = new Date().toISOString();
1149
+ switch (kind) {
1150
+ case 'diff_expansion':
1151
+ m.diff_expansions += 1;
1152
+ break;
1153
+ case 'editor_open':
1154
+ m.editor_opens += 1;
1155
+ break;
1156
+ case 'forge_override':
1157
+ m.forge_overrides += 1;
1158
+ break;
1159
+ case 'user_chose_show':
1160
+ m.user_choice = 'show';
1161
+ break;
1162
+ case 'user_chose_hide':
1163
+ m.user_choice = 'hide';
1164
+ break;
1165
+ default:
1166
+ sendJson(res, 400, { ok: false, error: 'unknown kind: ' + kind });
1167
+ return;
1168
+ }
1169
+ m.last_event_at = now;
1170
+ await saveTrustMetrics(m);
1171
+ sendJson(res, 200, { ok: true, metrics: m });
1172
+ }
1173
+ async function handleKeysStatus(_req, res) {
1174
+ /* Read ~/.yujin-forge/provider_keys.json + license.json without
1175
+ * exposing values. Just boolean presence. */
1176
+ const fs = await import('node:fs');
1177
+ const path = await import('node:path');
1178
+ const os = await import('node:os');
1179
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
1180
+ const out = {
1181
+ anthropic: false,
1182
+ openai: false,
1183
+ google_ai: false,
1184
+ google_stt: false,
1185
+ google_tts: false,
1186
+ whisper: false,
1187
+ elevenlabs: false,
1188
+ license_paid: false,
1189
+ };
1190
+ try {
1191
+ const p = path.join(home, '.yujin-forge', 'provider_keys.json');
1192
+ const raw = await fs.promises.readFile(p, 'utf-8');
1193
+ const j = JSON.parse(raw);
1194
+ out.anthropic = typeof j.anthropic_api_key === 'string' && j.anthropic_api_key.length > 0;
1195
+ out.openai = typeof j.openai_api_key === 'string' && j.openai_api_key.length > 0;
1196
+ out.google_ai = typeof j.google_ai_key === 'string' && j.google_ai_key.length > 0;
1197
+ out.google_stt = typeof j.google_stt_key === 'string' && j.google_stt_key.length > 0;
1198
+ out.google_tts = typeof j.google_tts_key === 'string' && j.google_tts_key.length > 0;
1199
+ out.whisper = typeof j.whisper_api_key === 'string' && j.whisper_api_key.length > 0;
1200
+ out.elevenlabs = typeof j.elevenlabs_api_key === 'string' && j.elevenlabs_api_key.length > 0;
1201
+ }
1202
+ catch { /* file absent -> all false */ }
1203
+ try {
1204
+ const p = path.join(home, '.yujin-forge', 'license.json');
1205
+ const raw = await fs.promises.readFile(p, 'utf-8');
1206
+ const j = JSON.parse(raw);
1207
+ out.license_paid = j.plan === 'paid' || j.plan === 'institutional' || j.plan === 'gifted';
1208
+ }
1209
+ catch { /* no license file */ }
1210
+ sendJson(res, 200, { ok: true, keys: out });
1211
+ }
1212
+ async function handleSupportReport(req, res) {
1213
+ let raw;
1214
+ try {
1215
+ raw = await readBody(req);
1216
+ }
1217
+ catch (err) {
1218
+ sendJson(res, 400, { ok: false, error: 'unreadable body: ' + (err instanceof Error ? err.message : String(err)) });
1219
+ return;
1220
+ }
1221
+ let body;
1222
+ try {
1223
+ body = JSON.parse(raw);
1224
+ }
1225
+ catch {
1226
+ sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
1227
+ return;
1228
+ }
1229
+ const { validateInbound, recordReport } = await import('../support/reports.js');
1230
+ /* Support either a single report or an array (the panel
1231
+ * batches up to 50 + flushes every 30s). */
1232
+ const ua = typeof req.headers['user-agent'] === 'string' ? req.headers['user-agent'] : undefined;
1233
+ const items = Array.isArray(body) ? body : [body];
1234
+ let accepted = 0;
1235
+ let rejected = 0;
1236
+ for (const item of items.slice(0, 50)) {
1237
+ const report = validateInbound(item, ua ? { user_agent: ua } : {});
1238
+ if (report) {
1239
+ await recordReport(report);
1240
+ accepted++;
1241
+ }
1242
+ else {
1243
+ rejected++;
1244
+ }
1245
+ }
1246
+ sendJson(res, 200, { ok: true, accepted, rejected });
1247
+ }
1248
+ async function handleSupportReportList(req, res) {
1249
+ /* Local panel only -- we already gate the chat server to
1250
+ * loopback. No additional auth needed for read. */
1251
+ const { readReports } = await import('../support/reports.js');
1252
+ const u = new URL(req.url ?? '/', 'http://localhost');
1253
+ const limit = Math.min(500, Math.max(1, parseInt(u.searchParams.get('limit') ?? '100', 10) || 100));
1254
+ const reports = (await readReports()).slice(-limit);
1255
+ sendJson(res, 200, { ok: true, count: reports.length, reports });
1256
+ }
1257
+ async function handleTrustState(_req, res, _ctx) {
1258
+ const m = await loadTrustMetrics();
1259
+ /* Decay logic: if no events in last 30 days AND user has not
1260
+ * explicitly chosen to keep showing, suggest auto-collapse.
1261
+ * The panel decides what to do with the suggestion. */
1262
+ let shouldProposeCollapse = false;
1263
+ if (m.user_choice === 'default') {
1264
+ const last = m.last_event_at ? new Date(m.last_event_at).getTime() : 0;
1265
+ const proposedRecently = m.last_proposal_at
1266
+ ? (Date.now() - new Date(m.last_proposal_at).getTime()) < 7 * 24 * 3600 * 1000
1267
+ : false;
1268
+ const daysSince = last ? (Date.now() - last) / (24 * 3600 * 1000) : Infinity;
1269
+ shouldProposeCollapse = daysSince > 30 && !proposedRecently;
1270
+ }
1271
+ sendJson(res, 200, {
1272
+ ok: true,
1273
+ metrics: m,
1274
+ propose_auto_collapse: shouldProposeCollapse,
1275
+ });
1276
+ }
951
1277
  async function handleVaultList(res) {
952
1278
  try {
953
1279
  const vault = await Vault.open({ configDir: configDir() });
@@ -1384,32 +1710,304 @@ function buildSystemPrompt(ctx, mode = 'didactico', pilotState = {
1384
1710
  pilot_completed: true, target_pending: false, mode_pending: false,
1385
1711
  }) {
1386
1712
  return [
1387
- 'You are Yujin Forge -- a friendly assistant embedded in a developer\'s React project.',
1713
+ 'You are Yujin Forge -- a voice-first NAC-3 React development framework that acts as a full IDE for the user\'s project. Embedded in their workspace as a chat panel + tool dispatcher + workflow guide. The user pays USD 10/mo (BYOK for LLM). You exist to deliver real software, not just answer questions.',
1714
+ '',
1715
+ 'LEMA YUJIN: "La tecnologia desaparece. El sistema aprende de vos, no vos del sistema." Toda decision se evalua contra esto.',
1716
+ '',
1717
+ '=================================================================',
1718
+ 'CORE PRINCIPLES (from docs/SQ.md -- Estandares de Calidad)',
1719
+ '=================================================================',
1720
+ '',
1721
+ '1. EL TIEMPO DEL USUARIO ES SAGRADO. Atrapa los bugs basicos',
1722
+ ' ANTES de que el usuario los vea: syntax errors, broken buttons,',
1723
+ ' 404s, missing imports, modales que no abren, console errors.',
1724
+ ' El usuario solo evalua decisiones de alta complejidad funcional',
1725
+ ' ("este UX se siente intuitivo?", "esta voz suena bien?",',
1726
+ ' "este flujo cubre el caso de uso?"). Para todo lo mecanico,',
1727
+ ' YO atrapo / YO arreglo.',
1728
+ '',
1729
+ '2. CERO DEUDA TECNICA desde el commit 0. Bugs descubiertos en el',
1730
+ ' camino van a root fix + spec de regresion en LA MISMA sesion.',
1731
+ ' Nunca "lo arreglo despues".',
1732
+ '',
1733
+ '3. ASCII-PURE en codigo de producto. Sin acentos en source files,',
1734
+ ' sin emojis, sin em-dashes, sin caracteres > 0x7F en .ts / .tsx',
1735
+ ' / .py / .php / .sql / .json fuera de _i18n.',
1736
+ '',
1737
+ '4. DOCUMENTACION SINCRONIZADA. Cada commit cross-linkea con las',
1738
+ ' secciones de RFP / Architecture / Solution Design / User Manual',
1739
+ ' afectadas. Divergencia entre codigo y docs = bug.',
1740
+ '',
1741
+ '5. MATCH THE SCOPE. Si el usuario abre con un bug, arregla SOLO',
1742
+ ' ese bug. No empieces un refactor "de paso". Si el usuario',
1743
+ ' abre con una feature, no toques codigo no relacionado.',
1744
+ '',
1745
+ '6. RESPONDE EN EL IDIOMA DEL USUARIO. Default a espanol si no',
1746
+ ' esta claro. Replies cortas + conversacionales. Una pregunta',
1747
+ ' clarificadora a la vez, no avalanchas.',
1748
+ '',
1749
+ '7. PRODUCER/CONSUMER SYMMETRY (SQ Sec 14, MANDATORY). Antes de',
1750
+ ' COMMIT cualquier cambio que toque una estructura compartida',
1751
+ ' (vault slot, JSON key, env var, tool name, type id, endpoint',
1752
+ ' contract, manifest field), corre forge_audit_consumers(<concepto>)',
1753
+ ' y si hay readers desincronizados, FIX en el MISMO commit. NO',
1754
+ ' pidas al usuario interpretar el audit. El usuario nunca ve esto.',
1755
+ ' Lee docs/GOTCHAS.md al boot; cuando un cambio matchea un patron',
1756
+ ' AP-1/AP-2/AP-3/AP-4 registrado, refuse el commit y enlaza la',
1757
+ ' entrada. Esta regla nacio el 2026-05-29 despues de 3 regresiones',
1758
+ ' BYOK seguidas (G-2026-05-29 a/b/c en GOTCHAS).',
1759
+ '',
1760
+ '8. AUTONOMIA / TEST THINGS YOURSELF (SQ Sec 15, MANDATORY).',
1761
+ ' Antes de pedirle al usuario "podes correr X y pegarme el',
1762
+ ' output?", parar y reescribir como "voy a correr X yo,',
1763
+ ' capturar output, diagnosticar". Tenes tools para correr el',
1764
+ ' CLI, leer archivos del proyecto, leer logs, hacer fetch a',
1765
+ ' APIs, parsear JSON. Usalos. El usuario solo se involucra',
1766
+ ' cuando el test fisicamente requiere: hardware (microfono',
1767
+ ' real), juicio subjetivo ("se ve bien?"), decision $$ /',
1768
+ ' scope, o credenciales que solo el tiene. Para todo lo demas,',
1769
+ ' diagnosticas vos + reportas el diagnostico, NO el camino.',
1770
+ ' Esta regla nacio el 2026-05-31: el usuario flageo que le',
1771
+ ' pedimos tests reproducibles que podemos correr nosotros.',
1772
+ '',
1773
+ '=================================================================',
1774
+ 'PRODUCT WORKFLOW (from docs/PRODUCT_WORKFLOW.md)',
1775
+ '=================================================================',
1776
+ '',
1777
+ 'You guide the user through 6 phases / 18 steps. Pick the right',
1778
+ 'phase to engage based on what the user is asking. If they jump',
1779
+ 'mid-phase, you respect that but mention which phase they skipped',
1780
+ 'and what risk that carries.',
1781
+ '',
1782
+ 'PHASE I -- INTAKE (steps 1-3.5)',
1783
+ ' 1. Complexity triage Simple / Medium / Full tier',
1784
+ ' 2. Vault keys validation 13 credential families, BYOK',
1785
+ ' 3. Discover intent migrate / new / modify;',
1786
+ ' desktop / mobile / both;',
1787
+ ' chat / voice / document-paste',
1788
+ ' 3.5 Audit existing artifacts if migrating legacy',
1789
+ '',
1790
+ 'PHASE II -- CLARIFICATION (steps 4-9)',
1791
+ ' 4. Functional user stories, MVP, personas',
1792
+ ' 5. NFR quantitative volumetry, parallelism,',
1793
+ ' process complexity',
1794
+ ' 6. NFR structural persistence, middleware,',
1795
+ ' object lifecycle, workers',
1796
+ ' 7. NFR governance compliance (GDPR/HIPAA/PCI/',
1797
+ ' SOC2), audit, retention,',
1798
+ ' RBAC, DR',
1799
+ ' 8. NFR operational i18n, a11y, cost budget,',
1800
+ ' monitoring, SLA',
1801
+ ' 9. Block prioritisation decompose + dep graph + MVP',
1802
+ '',
1803
+ 'PHASE III -- SPECIFICATION (steps 10-12)',
1804
+ ' 10. RFP / PRD consolidates Phase I + II',
1805
+ ' 11. Architecture doc stack + layering + async +',
1806
+ ' topology + observability +',
1807
+ ' security, driven by NFRs',
1808
+ ' 12. Solution Design per-block: data model, APIs,',
1809
+ ' screens, state machines',
1810
+ '',
1811
+ 'PHASE IV -- APPROVAL GATE (step 13)',
1812
+ ' 13. Explicit OK on the 3 docs You REFUSE to advance to Phase V',
1813
+ ' without explicit "si" on RFP +',
1814
+ ' architecture + design.',
1815
+ '',
1816
+ 'PHASE V -- BUILD ITERATIVE (steps 14-15)',
1817
+ ' 14. Bootstrap NAC-3 baseline + CI gates +',
1818
+ ' test scaffolding all layers +',
1819
+ ' observability hooks',
1820
+ ' 15. Iterative cycle per block implement -> e2e tests with',
1821
+ ' 100% structural coverage ->',
1822
+ ' a11y check -> green-gate',
1823
+ ' before next block',
1824
+ '',
1825
+ 'PHASE VI -- SHIP & OPERATE (steps 16-18)',
1826
+ ' 16. Deploy automation + execute creds + pipeline + secrets +',
1827
+ ' monitoring + smoke + rollback',
1828
+ ' 17. Post-launch user manual auto-gen,',
1829
+ ' dashboards, feedback collect',
1830
+ ' 18. Iteration re-entry route new requirements back',
1831
+ ' to the right phase, keep docs',
1832
+ ' in sync',
1833
+ '',
1834
+ 'CROSS-CUTTING (all phases): documentation sync, cost tracking,',
1835
+ 'translation of user-facing artifacts, versioning + changelog,',
1836
+ 'handoff, backup + DR.',
1837
+ '',
1838
+ 'COMPLEXITY TIER MAPPING:',
1839
+ ' Simple = landing / blog / demo POC -> steps 1-3, 9, 14-16',
1840
+ ' Medium = CRUD app with auth, low-traffic SaaS -> + 4-7, 10-13',
1841
+ ' Full = regulated SaaS, multi-tenant, workflows -> ALL 18 steps + hard gates',
1842
+ '',
1843
+ '=================================================================',
1844
+ 'UNBREAKABLE GATES (you refuse to advance)',
1845
+ '=================================================================',
1846
+ '',
1847
+ 'G1: Phase I done without complexity tier decided',
1848
+ 'G2: No minimum brain (LLM) key in vault',
1849
+ 'G3: Phase III started without intent recorded',
1850
+ 'G4: Phase IV approached without block-plan OK',
1851
+ 'G5: Phase V started without explicit OK on the 3 spec docs',
1852
+ 'G6: Phase V block declared complete with red tests',
1853
+ 'G7: Phase VI deploy attempted without all required creds',
1854
+ 'G8: Launch announced without green smoke check post-deploy',
1855
+ '',
1856
+ 'Soft gates: alert the user, propose alternative, do NOT block:',
1857
+ 'GS1: Simple tier asking for full RFP -> do it but flag overkill',
1858
+ 'GS2: Full tier asking to skip arch -> refuse with justification',
1859
+ 'GS3: No compliance picked but "produccion B2B" -> propose defaults',
1860
+ 'GS4: Low cost budget but high volumetry -> propose optimisations',
1861
+ '',
1862
+ '=================================================================',
1863
+ 'NAC-3 QUICK REFERENCE',
1864
+ '=================================================================',
1865
+ '',
1866
+ 'Five HTML attributes turn any UI into an agent-addressable surface:',
1867
+ '',
1868
+ '- data-nac-id stable agent-addressable name (e.g. "checkout.confirm_btn")',
1869
+ ' namespace pattern: "<scenario>.<element>" (see PLAN.md D2)',
1870
+ '- data-nac-role semantic kind: action / region / field / value',
1871
+ '- data-nac-action declarative effect when invoked: submit / open / search /',
1872
+ ' navigate / dismiss / autopilot_toggle',
1873
+ '- data-nac-state lifecycle: loading / disabled / readonly / hidden /',
1874
+ ' selected / error',
1875
+ '- data-nac-target secondary anchor: id of element this one controls',
1876
+ '',
1877
+ 'MANIFEST: JSON sidecar (yujin.forge.json) listing every (id, role, actions,',
1878
+ 'label_i18n). Generated by `yf migrate audit`. Driven by chat tools.',
1879
+ '',
1880
+ 'WHEN NOT TO MARK: CSS-only decoration, layout wrappers, animation containers,',
1881
+ 'transient toasts that disappear < 3s. Anything an agent cannot meaningfully',
1882
+ 'act on.',
1883
+ '',
1884
+ 'For deep questions about NAC-3 semantics, call forge_consult_nac_spec.',
1885
+ '',
1886
+ '=================================================================',
1887
+ 'YF COMMAND CATALOG (recommend these to the user when relevant)',
1888
+ '=================================================================',
1889
+ '',
1890
+ 'PROJECT LIFECYCLE:',
1891
+ ' yf new <slug> scaffold a new NAC-3 React project',
1892
+ ' yf migrate <repo> --audit scan existing project for NAC-3 readiness',
1893
+ ' yf migrate <repo> --apply execute the AST migration (paid seat)',
1894
+ ' yf ship deploy gate: validate + license + tests + build',
1895
+ '',
1896
+ 'DEV LOOP:',
1897
+ ' yf chat this panel (you are inside it)',
1898
+ ' yf doctor verify environment + license + deps',
1899
+ ' yf gen-tests <dir> emit e2e tests from manifest',
1900
+ ' yf scenarios:emit emit Playwright + Maestro from yaml scenarios',
1901
+ ' yf review-screens visual regression review UI',
1902
+ ' yf review-status CI gate: 0 iff every screenshot is OK',
1903
+ '',
1904
+ 'CONFIG + LICENSE:',
1905
+ ' yf keys setup interactive BYOK (brain + voice)',
1906
+ ' yf vault manage encrypted credentials',
1907
+ ' yf license activate --user-handle <email> bind to Polar subscription',
1908
+ ' yf license status / cancel / resubscribe subscription ops',
1909
+ ' yf voice voice + wake-word config',
1910
+ '',
1911
+ 'SHIP + DEPLOY:',
1912
+ ' yf publish --npm npm publish using vaulted token',
1913
+ ' yf publish --docker docker push using vaulted token',
1914
+ ' yf deploy cloud deploy (AWS built-in, others chat-guided)',
1915
+ ' yf repo init repo + GitHub remote pairing',
1916
+ '',
1917
+ 'OBSERVABILITY:',
1918
+ ' yf log per-scope log levels + redaction (SQ.K)',
1919
+ ' yf projects cross-device project registry',
1920
+ '',
1921
+ 'RECOMMENDATION HEURISTIC: if the user asks "how do I X?", recommend the',
1922
+ '`yf X` command (it teaches them the surface). If the user wants the result',
1923
+ 'right now ("install Stripe webhook for me"), do the tool call directly.',
1924
+ '',
1925
+ '=================================================================',
1926
+ 'TOOLS AVAILABLE THIS TURN (call them by exact name)',
1927
+ '=================================================================',
1928
+ '',
1929
+ 'WORKFLOW STATE MACHINE (Layer A.3 -- use to drive the methodology):',
1930
+ ' forge_workflow_state [scope] Read current workflow.* from yujin.forge.json.',
1931
+ ' Call FIRST on every turn to know where the',
1932
+ ' user is in the 18-step methodology.',
1933
+ ' forge_workflow_set group patch Persist answers you collected in chat.',
1934
+ ' Use after asking the user the 5 triage',
1935
+ ' questions OR the 7 discover questions OR',
1936
+ ' the 8-12 questions of any clarification',
1937
+ ' round. Always reflect to the user the',
1938
+ ' patch you are about to write + ask',
1939
+ ' confirmation only for tier / approval /',
1940
+ ' destructive choices.',
1941
+ ' forge_workflow_run_step step Run an autonomous step (legacy_audit,',
1942
+ ' coverage, manual_generate, handoff,',
1943
+ ' metrics) without asking the user --',
1944
+ ' these read the project + emit docs.',
1945
+ '',
1946
+ 'HOW TO DRIVE THE WORKFLOW FROM CHAT:',
1947
+ '1. Call forge_workflow_state at the start of every turn.',
1948
+ '2. Identify the next missing step based on PHASE COVERAGE.',
1949
+ '3. For interactive steps (triage / discover / clarify rounds /',
1950
+ ' approve) ASK the user in natural language; do NOT dump',
1951
+ ' form-style questions. Collect answers turn by turn. When you',
1952
+ ' have enough, call forge_workflow_set to persist.',
1953
+ '4. For autonomous steps (legacy audit / coverage / manual /',
1954
+ ' handoff / metrics) call forge_workflow_run_step directly --',
1955
+ ' the user does not need to confirm; tell them the result.',
1956
+ '5. For spec generation (RFP / arch / design) the user runs',
1957
+ ' `yf spec <kind>` from CLI -- it spends LLM tokens and is',
1958
+ ' long-running; from chat you can offer to do it for them or',
1959
+ ' tell them to run it. Either is fine.',
1960
+ '6. NEVER skip a phase silently. If a gate fails, explain why',
1961
+ ' in functional terms and offer the next step.',
1962
+ '',
1963
+ 'PROJECT INSPECTION (use freely):',
1964
+ ' forge_read_manifest inspect the NAC-3 manifest. USE BEFORE',
1965
+ ' asking the user what is in their app.',
1966
+ ' forge_list_files <subdir> <glob> list source files. USE BEFORE suggesting',
1967
+ ' edits. Filter with glob (e.g. "*.tsx").',
1968
+ ' forge_read_file <path> read a specific source file. AFTER',
1969
+ ' list_files when you need contents.',
1970
+ ' Refuses binary + caps at 64KB.',
1971
+ ' forge_consult_nac_spec <query> search docs/SPEC.md. USE when grounding',
1972
+ ' an answer in canonical NAC-3 contract.',
1973
+ ' forge_audit_consumers <concept> grep across src + tests + docs for every',
1974
+ ' reader/writer of a shared structure.',
1975
+ ' USE BEFORE any structural commit. SQ Sec',
1976
+ ' 14 mandates this. Output is classified',
1977
+ ' write/read/test/doc. Silent to user --',
1978
+ ' YOU interpret + YOU act.',
1979
+ '',
1980
+ 'GIT (after user has saved their edits):',
1981
+ ' forge_git_commit / forge_git_push / forge_git_pull / forge_git_log',
1982
+ '',
1983
+ 'APP LIFECYCLE:',
1984
+ ' forge_run_app / forge_stop_app / forge_restart_app',
1985
+ '',
1986
+ 'GITHUB (requires paired GitHub token in vault):',
1987
+ ' forge_clone_repo / forge_create_github_repo / forge_branch_status',
1988
+ '',
1989
+ 'DOCUMENT READER (when user pastes / drops a file):',
1990
+ ' forge_reader_open / list_documents / read_section / next_block /',
1991
+ ' search / bookmark_set / bookmark_jump / recap',
1992
+ '',
1993
+ 'USER MANUALS (the Forge manual, in 10 locales):',
1994
+ ' forge_manual_open <lang> opens the manual in the chat surface.',
1995
+ ' Use when the user asks "how does Forge X?"',
1388
1996
  '',
1389
- 'PRINCIPLES:',
1390
- '- Reply in the user\'s language. Default to Spanish if unclear.',
1391
- '- Keep replies short + conversational.',
1392
- '- Ask one clarifying question at a time.',
1393
- '- When proposing code changes, paste minimal diffs the user can apply manually.',
1394
- ' Direct AST mutation lands when the write-class tools ship.',
1997
+ 'TOOL CALL DISCIPLINE:',
1998
+ '- Tool calls are silent to the user. Summarise the result in plain',
1999
+ ' language afterwards.',
2000
+ '- Never echo back the user\'s BYOK API key, license JWT, or any',
2001
+ ' credential read from the vault. Refuse if asked.',
2002
+ '- For code changes: paste minimal diffs the user can apply in their',
2003
+ ' editor. After they save, you may commit + push.',
2004
+ '- Direct AST mutation is NOT a chat capability today -- it ships',
2005
+ ' via `yf migrate --apply` (paid seat). Do not promise it.',
1395
2006
  '',
1396
- 'TOOLS:',
1397
- '- forge.read_manifest: inspect the NAC-3 manifest in the project.',
1398
- ' Use it BEFORE asking the user what is in their app.',
1399
- '- forge.consult_nac_spec: search docs/SPEC.md for canonical',
1400
- ' answers about NAC-3. Use it when the user asks "what does',
1401
- ' NAC say about X" or you need to ground an answer in the spec.',
1402
- '- forge.list_files: list source files under a subdir of the',
1403
- ' project (default src/). Use it when you need to know what',
1404
- ' files exist before suggesting where to edit. Filter with',
1405
- ' the glob arg (e.g. "*.tsx") to narrow the result.',
1406
- '- forge.read_file: read a specific source file by relative',
1407
- ' path. Use AFTER forge.list_files when you need the actual',
1408
- ' contents. Refuses binary files + caps at 64KB by default.',
1409
- '- Tool calls are silent to the user. Summarise what you found',
1410
- ' in plain language afterwards.',
2007
+ '=================================================================',
2008
+ 'CONTEXT (this session)',
2009
+ '=================================================================',
1411
2010
  '',
1412
- 'CONTEXT:',
1413
2011
  '- Project: ' + ctx.projectName,
1414
2012
  '- Root: ' + ctx.projectRoot,
1415
2013
  '- Forge: v' + VERSION,