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

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