@nac3/forge-cli 1.0.47 → 1.0.49

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.
@@ -74,6 +74,19 @@ interface Ctx {
74
74
  setBrainProvider?: (mode: 'plan' | 'console') => Promise<boolean>;
75
75
  bridge?: import('./bridge.js').BridgeHandle | null;
76
76
  }
77
+ /** Pure ownership decision for a mobile chat command across N desktop
78
+ * instances (Pablo 2026-06-23). Exactly one instance must run each command.
79
+ * Run when: no slug (legacy mobile -> old broadcast); the slug is MY active
80
+ * project; I am alone / no bridge; or it is an orphan project (no live peer
81
+ * owns it) and I am the lowest-id instance. Skip when a live peer owns it. */
82
+ export declare function ownsMobileCommand(opts: {
83
+ projectSlug?: string;
84
+ activeSlug: string | null;
85
+ peerSlugs: Set<string>;
86
+ liveIds: string[];
87
+ selfId: string;
88
+ hasBridge: boolean;
89
+ }): boolean;
77
90
  export declare function buildSystemPrompt(ctx: Ctx, mode?: ForgeMode, pilotState?: {
78
91
  pilot_completed: boolean;
79
92
  target_pending: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/chat/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAA2D,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAKjG,OAAO,EAAoD,KAAK,aAAa,EAAoB,MAAM,aAAa,CAAC;AAErH,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,EAEL,KAAK,SAAS,EACf,MAAM,iBAAiB,CAAC;AAczB,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAsBjD,OAAO,EAAyD,KAAK,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAc1H,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB;;;;;;;;wDAQoD;IACpD,YAAY,CAAC,EAAE,eAAe,GAAG,iBAAiB,CAAC;IACnD;;2DAEuD;IACvD,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B;+CAC2C;IAC3C,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;;;kEAG8D;IAC9D,OAAO,CAAC,EAAE,OAAO,sBAAsB,EAAE,OAAO,CAAC;CAClD;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ;qEACiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,eAAe,CAAC;IACvB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAqBD,wBAAsB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CAoahF;AAED,UAAU,GAAG;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,aAAa,CAAC;IACtB,KAAK,EAAE,WAAW,CAAC;IACnB,KAAK,EAAE,eAAe,CAAC;IAIvB,OAAO,CAAC,EAAE,OAAO,sBAAsB,EAAE,OAAO,CAAC;IAMjD,YAAY,CAAC,EAAE,eAAe,GAAG,iBAAiB,CAAC;IAKnD,cAAc,CAAC,EAAE,MAAM,CAAC;IAGxB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAG3G,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAGlE,MAAM,CAAC,EAAE,OAAO,aAAa,EAAE,YAAY,GAAG,IAAI,CAAC;CACpD;AA60JD,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,GAAG,EACR,IAAI,GAAE,SAAuB,EAC7B,UAAU,GAAE;IAAE,eAAe,EAAE,OAAO,CAAC;IAAC,cAAc,EAAE,OAAO,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAErF;AACD;;;qDAGqD;AACrD,KAAK,CAAC,EAAE;IAAE,IAAI,EAAE,KAAK,GAAG,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AACrE;;;;;2CAK2C;AAC3C,sBAAsB,GAAE,MAAW;AACnC;;;;;;;4DAO4D;AAC5D,YAAY,GAAE,OAAe;AAC7B;;;;;;;uDAOuD;AACvD,mBAAmB,GAAE,MAAW;AAChC;;;;kEAIkE;AAClE,gBAAgB,GAAE,MAAW;AAC7B;;;;;;;;;;;+BAW+B;AAC/B,SAAS,GAAE,QAAQ,GAAG,YAAY,GAAG,MAAiB;AACtD;;;;;mEAKmE;AACnE,eAAe,CAAC,EAAE,eAAe,GAChC,MAAM,CAsgBR;AAED;;;;2EAI2E;AAC3E,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,GAAG,MAAM,CAoB5F"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/chat/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAA2D,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAKjG,OAAO,EAAoD,KAAK,aAAa,EAAoB,MAAM,aAAa,CAAC;AAErH,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,EAEL,KAAK,SAAS,EACf,MAAM,iBAAiB,CAAC;AAczB,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAsBjD,OAAO,EAAyD,KAAK,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAc1H,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB;;;;;;;;wDAQoD;IACpD,YAAY,CAAC,EAAE,eAAe,GAAG,iBAAiB,CAAC;IACnD;;2DAEuD;IACvD,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B;+CAC2C;IAC3C,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;;;kEAG8D;IAC9D,OAAO,CAAC,EAAE,OAAO,sBAAsB,EAAE,OAAO,CAAC;CAClD;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ;qEACiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,eAAe,CAAC;IACvB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAqBD,wBAAsB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CA6ahF;AAED,UAAU,GAAG;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,aAAa,CAAC;IACtB,KAAK,EAAE,WAAW,CAAC;IACnB,KAAK,EAAE,eAAe,CAAC;IAIvB,OAAO,CAAC,EAAE,OAAO,sBAAsB,EAAE,OAAO,CAAC;IAMjD,YAAY,CAAC,EAAE,eAAe,GAAG,iBAAiB,CAAC;IAKnD,cAAc,CAAC,EAAE,MAAM,CAAC;IAGxB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAG3G,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAGlE,MAAM,CAAC,EAAE,OAAO,aAAa,EAAE,YAAY,GAAG,IAAI,CAAC;CACpD;AA0BD;;;;+EAI+E;AAC/E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAOV;AAq3JD,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,GAAG,EACR,IAAI,GAAE,SAAuB,EAC7B,UAAU,GAAE;IAAE,eAAe,EAAE,OAAO,CAAC;IAAC,cAAc,EAAE,OAAO,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAErF;AACD;;;qDAGqD;AACrD,KAAK,CAAC,EAAE;IAAE,IAAI,EAAE,KAAK,GAAG,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AACrE;;;;;2CAK2C;AAC3C,sBAAsB,GAAE,MAAW;AACnC;;;;;;;4DAO4D;AAC5D,YAAY,GAAE,OAAe;AAC7B;;;;;;;uDAOuD;AACvD,mBAAmB,GAAE,MAAW;AAChC;;;;kEAIkE;AAClE,gBAAgB,GAAE,MAAW;AAC7B;;;;;;;;;;;+BAW+B;AAC/B,SAAS,GAAE,QAAQ,GAAG,YAAY,GAAG,MAAiB;AACtD;;;;;mEAKmE;AACnE,eAAe,CAAC,EAAE,eAAe,GAChC,MAAM,CAsgBR;AAED;;;;2EAI2E;AAC3E,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,GAAG,MAAM,CAoB5F"}
@@ -440,7 +440,15 @@ export async function startChatServer(opts) {
440
440
  * restart needed). When the relay is off it is a cheap no-op timer. */
441
441
  let commandConsumerHandle = null;
442
442
  try {
443
- commandConsumerHandle = startDesktopCommandConsumer(opts.port);
443
+ commandConsumerHandle = startDesktopCommandConsumer(opts.port, {
444
+ /* live so a reanchor moves which commands this instance owns */
445
+ getActiveSlug: () => session.projectSlug,
446
+ /* bridgeHandle is assigned just below; the closure reads it lazily at
447
+ * refresh time (after boot), so multi-instance ownership works without
448
+ * reordering. Null bridge -> single-instance assumption (run all). */
449
+ getBridgeState: async () => (bridgeHandle ? bridgeHandle.state() : null),
450
+ getSelfId: () => bridgeHandle?.selfId ?? '',
451
+ });
444
452
  }
445
453
  catch {
446
454
  commandConsumerHandle = null;
@@ -575,8 +583,68 @@ function getRelayPublisher() {
575
583
  * paired phone's chat commands, runs each through THIS server's own
576
584
  * /api/chat (loopback, so the full BYOK brain + project context apply),
577
585
  * and publishes the reply back as chat_reply. Gated YF_RELAY=1. */
578
- function startDesktopCommandConsumer(port) {
586
+ /** Pure ownership decision for a mobile chat command across N desktop
587
+ * instances (Pablo 2026-06-23). Exactly one instance must run each command.
588
+ * Run when: no slug (legacy mobile -> old broadcast); the slug is MY active
589
+ * project; I am alone / no bridge; or it is an orphan project (no live peer
590
+ * owns it) and I am the lowest-id instance. Skip when a live peer owns it. */
591
+ export function ownsMobileCommand(opts) {
592
+ if (!opts.projectSlug)
593
+ return true;
594
+ if (opts.projectSlug === opts.activeSlug)
595
+ return true;
596
+ if (!opts.hasBridge || opts.liveIds.length <= 1)
597
+ return true;
598
+ if (opts.peerSlugs.has(opts.projectSlug))
599
+ return false;
600
+ const lowest = [...opts.liveIds].sort()[0];
601
+ return opts.selfId !== '' && opts.selfId === lowest;
602
+ }
603
+ function startDesktopCommandConsumer(port, coord = {}) {
579
604
  const deviceId = 'desktop-' + process.pid;
605
+ /* Multi-instance project integrity (Pablo 2026-06-23). The relay is keyed
606
+ * only by handle, so EVERY yf chat on the machine reads the SAME command
607
+ * stream -> a phone command would run on ALL of them (duplicate execution,
608
+ * N replies). With the per-turn project binding, the phone tags each chat
609
+ * command with its project_slug; this lets each instance decide if the
610
+ * command is "mine". Rule: run it when (a) no slug (legacy mobile, keep old
611
+ * broadcast), (b) the slug is MY active project, or (c) no OTHER live
612
+ * instance owns that project (single instance, or an orphan project where
613
+ * the lowest-id instance takes it). So exactly ONE instance runs each
614
+ * command. Decision is sync off a cached bridge snapshot refreshed below. */
615
+ let peerSnapshot = { slugs: new Set(), liveIds: [], hasBridge: false };
616
+ const PEER_FRESH_MS = 15_000;
617
+ const refreshPeers = async () => {
618
+ if (!coord.getBridgeState)
619
+ return;
620
+ try {
621
+ const st = await coord.getBridgeState();
622
+ if (!st || !Array.isArray(st.instances)) {
623
+ peerSnapshot = { slugs: new Set(), liveIds: [], hasBridge: false };
624
+ return;
625
+ }
626
+ const cut = Date.now() - PEER_FRESH_MS;
627
+ const live = st.instances.filter((i) => i && typeof i.last_seen === 'number' && i.last_seen >= cut);
628
+ const self = coord.getSelfId ? coord.getSelfId() : '';
629
+ peerSnapshot = {
630
+ slugs: new Set(live.filter((i) => i.id !== self).map((i) => String(i.project_slug || '')).filter(Boolean)),
631
+ liveIds: live.map((i) => i.id),
632
+ hasBridge: true,
633
+ };
634
+ }
635
+ catch { /* keep the last snapshot on a transient failure */ }
636
+ };
637
+ const peerTimer = setInterval(() => { void refreshPeers(); }, 2500);
638
+ if (typeof peerTimer.unref === 'function')
639
+ peerTimer.unref();
640
+ const ownsChatCommand = (projectSlug) => ownsMobileCommand({
641
+ ...(projectSlug ? { projectSlug } : {}),
642
+ activeSlug: coord.getActiveSlug ? (coord.getActiveSlug() ?? null) : null,
643
+ peerSlugs: peerSnapshot.slugs,
644
+ liveIds: peerSnapshot.liveIds,
645
+ selfId: coord.getSelfId ? coord.getSelfId() : '',
646
+ hasBridge: peerSnapshot.hasBridge,
647
+ });
580
648
  const pollCommands = async (since) => {
581
649
  const r = await relayPoll({ deviceId }, since, ['command']);
582
650
  return {
@@ -683,6 +751,22 @@ function startDesktopCommandConsumer(port) {
683
751
  const j = await resp.json().catch(() => ({}));
684
752
  return j && j.active ? 'Turno interrumpido.' : 'No habia ningun turno en curso.';
685
753
  }
754
+ if (action === 'voice_diag') {
755
+ /* Mobile/PWA mic diagnostic -> the SAME support buffer the panel uses,
756
+ * so the phone's mic failures are visible server-side (Pablo: "el mic
757
+ * web nunca anduvo, sacame el log del telefono"). Best-effort. */
758
+ try {
759
+ const { recordReport, buildVoiceDiagReport } = await import('../support/reports.js');
760
+ const rep = buildVoiceDiagReport(args);
761
+ if (!rep)
762
+ return 'voice_diag sin stage.';
763
+ await recordReport(rep);
764
+ return 'Diagnostico de mic registrado: ' + rep.message;
765
+ }
766
+ catch {
767
+ return 'No pude registrar el diagnostico de voz.';
768
+ }
769
+ }
686
770
  return 'Accion no soportada: ' + action;
687
771
  };
688
772
  /* Two consumers over the same stream (task #29): the CHAT consumer
@@ -693,7 +777,9 @@ function startDesktopCommandConsumer(port) {
693
777
  * past the skipped events. */
694
778
  const chat = startCommandConsumer({
695
779
  enabled: relayEnabled, intervalMs: 3000,
696
- accept: (c) => !c.control,
780
+ /* only MY project's chat commands (multi-instance integrity); the other
781
+ * instances' consumers advance their cursor past the skipped ones. */
782
+ accept: (c) => !c.control && ownsChatCommand(c.projectSlug),
697
783
  pollCommands, runTurn, reply,
698
784
  ackImmediate: true, /* confirm receipt before the turn runs */
699
785
  onError: () => { },
@@ -705,8 +791,8 @@ function startDesktopCommandConsumer(port) {
705
791
  onError: () => { },
706
792
  });
707
793
  return {
708
- stop() { chat.stop(); control.stop(); },
709
- tickOnce: async () => { await chat.tickOnce(); await control.tickOnce(); },
794
+ stop() { clearInterval(peerTimer); chat.stop(); control.stop(); },
795
+ tickOnce: async () => { await refreshPeers(); await chat.tickOnce(); await control.tickOnce(); },
710
796
  };
711
797
  }
712
798
  /* Project context for board snapshots, set per chat turn (handleChat). */