shraga 0.1.45 → 0.1.47

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shraga",
3
- "version": "0.1.45",
3
+ "version": "0.1.47",
4
4
  "description": "The teammate you delegate coding to — a self-hostable, multi-user AI coding agent web UI (Claude Code, with a pluggable engine seam).",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { hostname } from 'node:os';
3
4
  import { DATA_DIR } from './paths.ts';
4
5
  import { emitEvent } from './events/bus.ts';
5
6
  import { runTextQuery } from './sdk-utils.ts';
@@ -18,6 +19,15 @@ export class DataSyncOptions {
18
19
  * A real deployment must ALSO set DATA_SYNC_ENABLE=1. Dev/verify boots never sync/push.
19
20
  */
20
21
  enabled = process.env.DATA_SYNC_ENABLE === '1' || process.env.DATA_SYNC_ENABLE === 'true';
22
+ /**
23
+ * Owner-facing alerts are a PROD side effect, so only the designated single-writer instance
24
+ * (the same one that fires schedules) may DM owners. A dev laptop legitimately syncs the shared
25
+ * data repo, but it also shares .env's APP_NAME, OWNERS and the Slack bot token — so its alerts
26
+ * are indistinguishable from the box's. Past incident 2026-08-20: a laptop pinned to an older
27
+ * shraga re-sent a merge-conflict alert for a bug already fixed AND deployed on the box, and the
28
+ * DM gave no way to tell which instance sent it. Suppressed alerts are still logged locally.
29
+ */
30
+ notify = process.env.DATA_SYNC_SCHEDULER_ACTIVE === 'true';
21
31
  }
22
32
 
23
33
  export class DataSync {
@@ -97,8 +107,8 @@ export class DataSync {
97
107
  }
98
108
  this.ensureGitignore();
99
109
  if (!this.verifyDeploymentId()) return;
110
+ await this.configureGit(); // must precede untrackIgnored — it neutralises per-user excludes
100
111
  await this.untrackIgnored();
101
- await this.configureGit();
102
112
  this.ready = true;
103
113
  }
104
114
 
@@ -550,6 +560,12 @@ export class DataSync {
550
560
  }
551
561
 
552
562
  private async notifyOwners(text: string): Promise<void> {
563
+ if (!this.options.notify) {
564
+ // Not the authoritative instance — log it so a dev run still surfaces the problem locally,
565
+ // but never DM owners (see DataSyncOptions.notify).
566
+ console.warn(`${TAG} notification suppressed (not the authoritative instance):\n${text}`);
567
+ return;
568
+ }
553
569
  // No Slack coupling here — resolve owner contacts and publish a deploy notice on the event bus.
554
570
  // The Slack feature (slackFeature) subscribes and DMs each owner. Owner resolution uses the
555
571
  // contacts store only, so data-sync stays transport-agnostic.
@@ -562,7 +578,10 @@ export class DataSync {
562
578
  console.warn(`${TAG} No owners (OWNERS env) with Slack IDs found, skipping notification`);
563
579
  return;
564
580
  }
565
- emitEvent('data-sync', { kind: 'deploy', owners, text });
581
+ // Stamp the sender: every owner alert must name the instance it came from, so "is this back?"
582
+ // is answerable without log archaeology across hosts.
583
+ const from = `${this.options.deploymentId || 'shraga'}@${hostname()}`;
584
+ emitEvent('data-sync', { kind: 'deploy', owners, text: `${text}\n\n_from ${from}_` });
566
585
  }
567
586
 
568
587
  /**
@@ -605,7 +624,13 @@ export class DataSync {
605
624
  /** Commit a refreshed .gitignore and untrack any committed files that now match it. */
606
625
  private async untrackIgnored(): Promise<void> {
607
626
  await this.git('add', '.gitignore').catch(() => {});
608
- const out = await this.git('ls-files', '-i', '-c', '--exclude-standard').catch(() => '');
627
+ // core.excludesFile=/dev/null: --exclude-standard would otherwise fold in the USER'S global
628
+ // gitignore (~/.gitignore_global) and untrack files that are ignored on this host only. The
629
+ // data repo is shared, so a personal rule must never decide what leaves the remote. Real
630
+ // near-miss 2026-08-20: a laptop's global `.agent/` rule staged 50 files for deletion —
631
+ // including workspace/.agent/memory/MEMORY.md, the agent's own memory — and only
632
+ // guardMassDeletions() stopped it. Only the repo's OWN .gitignore may untrack anything.
633
+ const out = await this.git('-c', 'core.excludesFile=/dev/null', 'ls-files', '-i', '-c', '--exclude-standard').catch(() => '');
609
634
  // Only untrack files that actually exist on disk AND match .gitignore.
610
635
  // git ls-files -i can falsely report tracked files missing from the worktree
611
636
  // (remote-only files restored during init). Removing those wipes remote data.
@@ -654,6 +679,8 @@ export class DataSync {
654
679
  const name = process.env.APP_NAME || 'shraga';
655
680
  await this.git('config', 'user.name', `${name} agent`).catch(() => {});
656
681
  await this.git('config', 'user.email', `agent@${name}.local`).catch(() => {});
682
+ // The data repo must behave identically on every host: no per-user global gitignore.
683
+ await this.git('config', 'core.excludesFile', '/dev/null').catch(() => {});
657
684
  }
658
685
 
659
686
  private authedUrl(): string {