@nac3/forge-cli 1.8.4 → 1.8.6

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.
@@ -131,6 +131,49 @@ export async function startChatServer(opts) {
131
131
  * CLI + panel converge. */
132
132
  const { resolveProjectSlug } = await import('./project_slug.js');
133
133
  const projectSlug = await resolveProjectSlug(opts.projectRoot);
134
+ /* COLISION DE SLUG (Pablo 2026-10-04). El slug es la clave de identidad de
135
+ * TODO lo que es por proyecto: el grafo semantico, los pendings, el wiring,
136
+ * el cerebro, y la entrada del registro que alimenta el selector del
137
+ * celular. Dos carpetas distintas que resuelven al mismo slug comparten
138
+ * todo eso en silencio.
139
+ *
140
+ * Paso de verdad: C:\yujin-kikan (el CRM generico) y C:\adip\kikan (el
141
+ * adaptado para ADIP, que es una copia y por eso tiene el MISMO
142
+ * package.json name) resolvian los dos a 'yujin-kikan'. Resultado: un solo
143
+ * pending.json de 354KB y un solo grafo para los dos proyectos, y la
144
+ * entrada del registro apuntando a la carpeta de quien arranco ultimo -- el
145
+ * celular mostraba un proyecto y abria el otro.
146
+ *
147
+ * No se renombra nada solo: cambiarle el slug a un proyecto le huerfana el
148
+ * grafo y los pendings que ya tiene. Se AVISA, fuerte, con el arreglo
149
+ * exacto -- un `project_slug` explicito en yujin.forge.json, que el
150
+ * resolver respeta antes que cualquier otra cosa. */
151
+ try {
152
+ const reg = await readRegistry();
153
+ const choque = (reg.projects || []).find((p) => {
154
+ if (p.slug !== projectSlug || !p.path)
155
+ return false;
156
+ const norm = (s) => s.replace(/[\\/]+/g, '/').replace(/\/+$/, '').toLowerCase();
157
+ return norm(p.path) !== norm(opts.projectRoot);
158
+ });
159
+ if (choque) {
160
+ const aviso = '[yf] ATENCION: colision de slug. "' + projectSlug + '" ya estaba usado por '
161
+ + choque.path + ' y esta carpeta es ' + opts.projectRoot + '. Los dos proyectos comparten '
162
+ + 'grafo, pendings, wiring y cerebro, y el selector del celular no puede distinguirlos. '
163
+ + 'Arreglo: agregale "project_slug": "<un-slug-propio>" al yujin.forge.json de uno de los dos.';
164
+ try {
165
+ console.error(aviso);
166
+ }
167
+ catch { /* noop */ }
168
+ /* Que ademas llegue al chat, no solo al stdout que nadie mira. */
169
+ try {
170
+ const { enqueuePanelDispatch } = await import('./panel_queue.js');
171
+ enqueuePanelDispatch('system.chat-log', { text: aviso }, new Date().toISOString());
172
+ }
173
+ catch { /* best-effort */ }
174
+ }
175
+ }
176
+ catch { /* sin registro no hay con que comparar */ }
134
177
  /* Cerebro por instancia (Pablo, 2026-09-30). Habia una sola config para
135
178
  * todas: cambiar el cerebro en un `yf chat` lo cambiaba en todos. Este
136
179
  * proceso queda atado al cerebro de SU proyecto -- lee el del proyecto si
@@ -164,7 +207,20 @@ export async function startChatServer(opts) {
164
207
  * alpha.59v.2 -- registered via startAutoIngest so the
165
208
  * /api/forge/graph-status endpoint can await it (avoiding
166
209
  * the panel showing partial data during the ingest). */
210
+ /* AISLAMIENTO EN TESTS (Pablo 2026-10-04). Estos tres trabajos del boot son
211
+ * fire-and-forget y escriben en el store por slug (projects/<slug>/
212
+ * semantic_graph.db, pending.json). El path lo resuelven POR LLAMADA desde
213
+ * YF_CONFIG_DIR, pero sobreviven al test que los disparo: cuando corren, el
214
+ * afterEach ya restauro la env var, asi que terminan escribiendo en el
215
+ * config dir REAL del usuario. Evidencia: `projects/demo-scope/`,
216
+ * `projects/mode-console/`, `projects/lifecycle-test/` aparecieron en el
217
+ * ~/.yujin-forge de Pablo con fecha de las corridas de hoy. Y ese directorio
218
+ * es el que alimenta el selector de proyectos del celular, asi que la
219
+ * basura de los tests se ve en pantalla. En test no corren. */
220
+ const enTest = !!process.env.VITEST || process.env.NODE_ENV === 'test';
167
221
  try {
222
+ if (enTest)
223
+ throw new Error('test env: sin auto-ingest');
168
224
  const { startAutoIngest } = await import('./graph_auto_ingest.js');
169
225
  startAutoIngest({
170
226
  project_root: opts.projectRoot,
@@ -181,17 +237,18 @@ export async function startChatServer(opts) {
181
237
  * background, para que al abrir el panel el grafo este completo.
182
238
  * El boton Refresh + el watcher lo regeneran luego ante cambios.
183
239
  * Fire-and-forget; nunca rompe el boot. */
184
- void (async () => {
185
- try {
186
- const { regenerateAndPersist } = await import('../semantic_graph/derive.js');
187
- const s = await regenerateAndPersist({ project_root: opts.projectRoot, project_slug: projectSlug, enrich: false });
240
+ if (!enTest)
241
+ void (async () => {
188
242
  try {
189
- console.error('[graph-regen] boot -> ' + (s.module_edges + s.lib_edges) + ' edges (' + s.pruned + ' pruned)');
243
+ const { regenerateAndPersist } = await import('../semantic_graph/derive.js');
244
+ const s = await regenerateAndPersist({ project_root: opts.projectRoot, project_slug: projectSlug, enrich: false });
245
+ try {
246
+ console.error('[graph-regen] boot -> ' + (s.module_edges + s.lib_edges) + ' edges (' + s.pruned + ' pruned)');
247
+ }
248
+ catch { /* */ }
190
249
  }
191
- catch { /* */ }
192
- }
193
- catch { /* never break boot */ }
194
- })();
250
+ catch { /* never break boot */ }
251
+ })();
195
252
  /* alpha.59w -- fs.watch over src/components, src/hooks, src/lib,
196
253
  * src/core. When the user adds or modifies a TS/JS file, the
197
254
  * watcher extracts top-level exports (regex), creates one
@@ -200,6 +257,8 @@ export async function startChatServer(opts) {
200
257
  * attached to the returned StartedServer.close(). */
201
258
  let componentWatcherHandle = null;
202
259
  try {
260
+ if (enTest)
261
+ throw new Error('test env: sin component watcher');
203
262
  const { startComponentWatcher } = await import('./graph_component_watcher.js');
204
263
  componentWatcherHandle = await startComponentWatcher({
205
264
  project_root: opts.projectRoot,
@@ -366,6 +425,15 @@ export async function startChatServer(opts) {
366
425
  * ephemeral runs (vitest sets VITEST; also skip an obvious temp root). */
367
426
  const isTestEnv = !!process.env.VITEST || process.env.NODE_ENV === 'test';
368
427
  const isTempRoot = /[\\/](?:te?mp|yf-[a-z-]*proj|yf-startup)/i.test(opts.projectRoot);
428
+ /* El mismo guard, pero evaluable contra CUALQUIER root -- lo necesita el
429
+ * reanchor, que recibe uno nuevo (Pablo 2026-10-04). El reanchor escribia
430
+ * en el registro sin guard alguno, asi que un test que cambia de proyecto
431
+ * dejaba sus carpetas temporales en el registro REAL del usuario: y el
432
+ * registro alimenta el selector de proyectos del celular, de modo que la
433
+ * basura termina en pantalla. */
434
+ const puedeEscribirRegistro = (root) => !isTestEnv
435
+ && !/[\\/](?:te?mp|yf-[a-z-]*proj|yf-startup)/i.test(root)
436
+ && !opts.agentMode;
369
437
  /* A headless agent sub-session must NEVER touch the global registry --
370
438
  * it would flip the parent's active project (Pablo's scramble bug). */
371
439
  if (!isTestEnv && !isTempRoot && !opts.agentMode) {
@@ -410,11 +478,13 @@ export async function startChatServer(opts) {
410
478
  }
411
479
  catch { /* best-effort */ }
412
480
  /* keep the registry in sync after a hot switch (task #33) */
413
- try {
414
- await upsertProject({ slug: session.projectSlug, path: abs });
415
- await setActive(session.projectSlug);
481
+ if (puedeEscribirRegistro(abs)) {
482
+ try {
483
+ await upsertProject({ slug: session.projectSlug, path: abs });
484
+ await setActive(session.projectSlug);
485
+ }
486
+ catch { /* best-effort */ }
416
487
  }
417
- catch { /* best-effort */ }
418
488
  chatLog('Cambiando al proyecto ' + session.projectName + ' (' + abs + ')...');
419
489
  /* re-hidratar ingest + reiniciar el watcher sobre el nuevo dir */
420
490
  try {
@@ -459,6 +529,12 @@ export async function startChatServer(opts) {
459
529
  * terminar dispara el re-tabs. */
460
530
  const slugNow = session.projectSlug;
461
531
  const nameNow = session.projectName;
532
+ /* Mismo aislamiento que el regen del boot (2026-10-04): este tambien es
533
+ * fire-and-forget y sobrevive al test que lo disparo, asi que terminaba
534
+ * creando projects/<slug>/semantic_graph.db en el config dir REAL. Se vio
535
+ * con los slugs 'pin-to' y 'switch-to', del test de reanchor. */
536
+ if (enTest)
537
+ return { projectRoot: session.projectRoot, projectName: nameNow, projectSlug: slugNow };
462
538
  chatLog('Regenerando el grafo semantico de ' + nameNow + '...');
463
539
  void (async () => {
464
540
  try {
@@ -1083,7 +1159,16 @@ function publishBoardSnapshot() {
1083
1159
  snap.projects = (reg.projects || []).slice(0, 30).map((p) => ({
1084
1160
  slug: p.slug, path: p.path || '',
1085
1161
  }));
1086
- snap.activeProject = reg.active_slug || cx.slug || null;
1162
+ /* El proyecto activo que el celular espeja es el de ESTA instancia,
1163
+ * no el puntero global del registro (Pablo 2026-10-04: "la otra la
1164
+ * sincroniza mal, me sincroniza el yujin-kikan en vez del que
1165
+ * tengo"). Con dos `yf chat` abiertos, active_slug es el del que
1166
+ * arranco o cambio de proyecto ultimo, asi que las DOS instancias
1167
+ * publicaban el mismo activeProject: el celular, espejando una,
1168
+ * mostraba el proyecto de la otra. cx.slug es el de la instancia que
1169
+ * publica este board. El global queda de respaldo para el caso en
1170
+ * que la instancia todavia no tenga slug. */
1171
+ snap.activeProject = cx.slug || reg.active_slug || null;
1087
1172
  }
1088
1173
  catch { /* leave unset */ }
1089
1174
  /* Relay budget (bug 2026-07-10): the worker rejects events over 32KB
@@ -2244,6 +2329,12 @@ async function route(req, res, ctx) {
2244
2329
  month: status.month,
2245
2330
  renewal_at: status.renewal_at,
2246
2331
  extension_url: status.extension_url,
2332
+ /* 2026-10-04 -- `source` (y el plan del worker) no se reenviaban, asi
2333
+ * que el chip de fuente del panel era codigo muerto: la tarjeta no
2334
+ * podia decir si el numero venia del worker o del medidor local.
2335
+ * Justo el dato que hace falta cuando los tokens se ven en cero. */
2336
+ source: status.source,
2337
+ server_plan: status.server_plan,
2247
2338
  eco: {
2248
2339
  cap: finite(status.eco.cap),
2249
2340
  used: status.eco.used,
@@ -5925,6 +6016,12 @@ async function handleChat(req, res, ctx) {
5925
6016
  * this block was byte-identical turn-to-turn yet re-sent uncached. */
5926
6017
  systemDynamic: projectContextSection + wiringSection + toolingSection + recentSessionsRecap + graphSection + snapshotSection + workflowSection + approvalPostureSection + toolStatusSection(),
5927
6018
  maxTokens: _maxTokensPerTurn,
6019
+ /* Tope del turno en Plan, POR TURNO (Pablo 2026-10-04). Se leia solo al
6020
+ * construir el cliente, asi que subir claude_cli_timeout_ms en el panel
6021
+ * no hacia nada hasta reiniciar: el panel decia 10000000 y el corte
6022
+ * seguia avisando 5000000. Sale del mismo `_limits` que ya se lee cada
6023
+ * turno para max_tokens_per_turn. Los clientes HTTP lo ignoran. */
6024
+ timeoutMs: _limits.claude_cli_timeout_ms,
5928
6025
  /* Plan/CLI uses Claude Code's native tools (FORGE_TOOL_SPECS = just
5929
6026
  * nac3_invoke via the MCP bridge); the HTTP path (BYOK/Console + managed)
5930
6027
  * gets direct native file tools + nac3_invoke so the model is fluent with