claude-maestro 0.1.19 → 0.1.20

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 (28) hide show
  1. package/out/main/index.js +419 -56
  2. package/out/preload/index.js +3 -0
  3. package/out/renderer/assets/{index-CNNAMsV1.js → index--LsdCcT2.js} +2 -2
  4. package/out/renderer/assets/{index-Dhxn3JIv.js → index-2Q5NJLLa.js} +2 -2
  5. package/out/renderer/assets/{index-Dgaj6c_K.css → index-B4pxYlCv.css} +442 -2
  6. package/out/renderer/assets/{index-B59uuZRU.js → index-B9wQ40iJ.js} +4 -4
  7. package/out/renderer/assets/{index-DI2ly48w.js → index-BD5wVgZG.js} +2 -2
  8. package/out/renderer/assets/{index-CWk6CwGd.js → index-BKVqTAbd.js} +3 -3
  9. package/out/renderer/assets/{index-JMVyecfQ.js → index-BSnEdUx8.js} +5 -5
  10. package/out/renderer/assets/{index-C479DZmL.js → index-BUr9qTcP.js} +5 -5
  11. package/out/renderer/assets/{index-C0rsWi9C.js → index-BX4eMUiW.js} +2 -2
  12. package/out/renderer/assets/{index-CuHjzw7d.js → index-BYrBOYyo.js} +5 -5
  13. package/out/renderer/assets/{index-D9GPva9-.js → index-BdkQGOfF.js} +5 -5
  14. package/out/renderer/assets/{index-CTxGDYbk.js → index-BeXyvqqV.js} +2026 -1461
  15. package/out/renderer/assets/{index-9AHdXE8U.js → index-C2xG1-2F.js} +2 -2
  16. package/out/renderer/assets/{index-CVWvgy2Y.js → index-CALj_g-h.js} +2 -2
  17. package/out/renderer/assets/{index-Bg4ondS2.js → index-CqEbh7gN.js} +2 -2
  18. package/out/renderer/assets/{index-1Z03T0zz.js → index-CwrdXZxY.js} +2 -2
  19. package/out/renderer/assets/{index-jAA5WJm3.js → index-DFnn9t0U.js} +5 -5
  20. package/out/renderer/assets/{index-CZP8wVw-.js → index-DMLpIsZn.js} +2 -2
  21. package/out/renderer/assets/{index-CoyUYEik.js → index-Dpj9EP42.js} +3 -3
  22. package/out/renderer/assets/{index-Cq5xQaOf.js → index-DwCelZNB.js} +5 -5
  23. package/out/renderer/assets/{index-BkOzhsuz.js → index-K7jPnu8A.js} +1 -1
  24. package/out/renderer/assets/{index-LW-gCnC-.js → index-KjJOaoK3.js} +2 -2
  25. package/out/renderer/assets/{index-DJwKAmOm.js → index-NXP0WfIH.js} +2 -2
  26. package/out/renderer/assets/{index-CXeHg_Qc.js → index-RU5VUPJx.js} +2 -2
  27. package/out/renderer/index.html +2 -2
  28. package/package.json +1 -1
package/out/main/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  const electron = require("electron");
3
+ const events = require("events");
3
4
  const path = require("path");
4
5
  const fs = require("fs");
5
6
  const os = require("os");
@@ -1009,8 +1010,8 @@ function resolveClaude() {
1009
1010
  if (!IS_WIN$1) {
1010
1011
  return candidates[0] ? { file: candidates[0], argsPrefix: [] } : null;
1011
1012
  }
1012
- const exe = candidates.find((c) => c.toLowerCase().endsWith(".exe"));
1013
- const cmd = candidates.find((c) => /\.(cmd|bat)$/i.test(c));
1013
+ const exe = candidates.find((c2) => c2.toLowerCase().endsWith(".exe"));
1014
+ const cmd = candidates.find((c2) => /\.(cmd|bat)$/i.test(c2));
1014
1015
  if (exe) return { file: exe, argsPrefix: [] };
1015
1016
  if (cmd) return { file: process.env.ComSpec ?? "cmd.exe", argsPrefix: ["/c", cmd] };
1016
1017
  return null;
@@ -1323,10 +1324,12 @@ const ALLOWED_TOOLS$2 = [
1323
1324
  "Bash(git rev-parse:*)"
1324
1325
  ];
1325
1326
  class AutoExpandService {
1326
- constructor(persistence2, features, getWin2) {
1327
+ constructor(persistence2, features, getWin2, emitGame = () => {
1328
+ }) {
1327
1329
  this.persistence = persistence2;
1328
1330
  this.features = features;
1329
1331
  this.getWin = getWin2;
1332
+ this.emitGame = emitGame;
1330
1333
  }
1331
1334
  timer = null;
1332
1335
  /** Next scheduled run (ms epoch) per session id. */
@@ -1523,6 +1526,7 @@ class AutoExpandService {
1523
1526
  run.finishedAt = Date.now();
1524
1527
  run.status = status;
1525
1528
  this.broadcast(run.sessionId);
1529
+ if (status === "done") this.emitGame({ type: "autoexpand.done" });
1526
1530
  }
1527
1531
  broadcast(sessionId) {
1528
1532
  this.getWin()?.webContents.send("autoexpand:runs", sessionId, this.listRuns(sessionId));
@@ -2021,9 +2025,9 @@ class ConductorService {
2021
2025
  const list = focusId ? this.sessions.list().filter((s) => s.config.id === focusId) : this.sessions.list();
2022
2026
  const sessions = await Promise.all(
2023
2027
  list.map(async (s) => {
2024
- const c = s.config;
2025
- const git2 = await this.sessions.getGitStatus(c.id).catch(() => null);
2026
- const feats = this.features.list(c.id).map((f) => ({
2028
+ const c2 = s.config;
2029
+ const git2 = await this.sessions.getGitStatus(c2.id).catch(() => null);
2030
+ const feats = this.features.list(c2.id).map((f) => ({
2027
2031
  id: f.id,
2028
2032
  title: f.title,
2029
2033
  status: f.status,
@@ -2031,19 +2035,19 @@ class ConductorService {
2031
2035
  openSpecs: f.specs.filter((sp) => !sp.done).length
2032
2036
  }));
2033
2037
  const entry = {
2034
- id: c.id,
2035
- name: c.name,
2036
- folder: c.folder,
2037
- isActive: c.id === activeId,
2038
+ id: c2.id,
2039
+ name: c2.name,
2040
+ folder: c2.folder,
2041
+ isActive: c2.id === activeId,
2038
2042
  status: s.status,
2039
- categoryId: c.categoryId ?? null,
2043
+ categoryId: c2.categoryId ?? null,
2040
2044
  terminals: s.terminals.map((t) => ({ kind: t.config.kind, status: t.status })),
2041
- worktree: c.worktree ? {
2042
- parentSessionId: c.worktree.parentSessionId,
2043
- branch: c.worktree.branch,
2044
- baseBranch: c.worktree.baseBranch
2045
+ worktree: c2.worktree ? {
2046
+ parentSessionId: c2.worktree.parentSessionId,
2047
+ branch: c2.worktree.branch,
2048
+ baseBranch: c2.worktree.baseBranch
2045
2049
  } : null,
2046
- autoExpand: c.autoExpand ? { enabled: c.autoExpand.enabled, branch: c.autoExpand.branch } : null,
2050
+ autoExpand: c2.autoExpand ? { enabled: c2.autoExpand.enabled, branch: c2.autoExpand.branch } : null,
2047
2051
  git: git2 ? {
2048
2052
  branch: git2.branch,
2049
2053
  ahead: git2.ahead,
@@ -2052,8 +2056,8 @@ class ConductorService {
2052
2056
  } : null,
2053
2057
  features: feats
2054
2058
  };
2055
- if (c.worktree) {
2056
- const task = await this.sessions.getWorktreeTaskState(c.id).catch(() => null);
2059
+ if (c2.worktree) {
2060
+ const task = await this.sessions.getWorktreeTaskState(c2.id).catch(() => null);
2057
2061
  if (task) {
2058
2062
  entry.task = {
2059
2063
  dirty: task.dirty,
@@ -2504,8 +2508,10 @@ const KNOWN_LABELS = {
2504
2508
  github: "GitHub"
2505
2509
  };
2506
2510
  class FactoryService {
2507
- constructor(getWin2) {
2511
+ constructor(getWin2, emitGame = () => {
2512
+ }) {
2508
2513
  this.getWin = getWin2;
2514
+ this.emitGame = emitGame;
2509
2515
  this.state = this.store.load();
2510
2516
  this.runs = restoreRuns(this.store.loadRuns());
2511
2517
  for (const s of this.state.suggestions) {
@@ -2623,23 +2629,23 @@ class FactoryService {
2623
2629
  absorbScanSuggestions(run) {
2624
2630
  let newest = null;
2625
2631
  let added = 0;
2626
- for (const c of run.candidates) {
2627
- if (c.status !== "proposed") continue;
2628
- if (this.suggestionDuplicate(c.kind, c.name, c.description)) continue;
2632
+ for (const c2 of run.candidates) {
2633
+ if (c2.status !== "proposed") continue;
2634
+ if (this.suggestionDuplicate(c2.kind, c2.name, c2.description)) continue;
2629
2635
  newest = this.enqueueSuggestion({
2630
- suggestedKind: c.kind,
2631
- name: c.name,
2632
- title: c.description,
2633
- description: c.description,
2634
- rationale: c.rationale,
2636
+ suggestedKind: c2.kind,
2637
+ name: c2.name,
2638
+ title: c2.description,
2639
+ description: c2.description,
2640
+ rationale: c2.rationale,
2635
2641
  origin: "scan",
2636
2642
  sourceRef: run.source,
2637
2643
  sourceLabel: run.sourceLabel,
2638
2644
  source: run.source,
2639
2645
  context: run.summary,
2640
- topics: c.topics,
2641
- keywords: c.keywords,
2642
- existing: c.existing,
2646
+ topics: c2.topics,
2647
+ keywords: c2.keywords,
2648
+ existing: c2.existing,
2643
2649
  confidence: 0.8
2644
2650
  });
2645
2651
  added++;
@@ -3262,7 +3268,7 @@ Lessons learned (respect these):
3262
3268
  /** Approve a candidate: author its file content and write it to ~/.claude. An errored candidate can be retried. */
3263
3269
  async approve(runId, candidateId) {
3264
3270
  const run = this.runs.find((r) => r.id === runId);
3265
- const candidate = run?.candidates.find((c) => c.id === candidateId);
3271
+ const candidate = run?.candidates.find((c2) => c2.id === candidateId);
3266
3272
  if (!run || !candidate || candidate.status !== "proposed" && candidate.status !== "error") return;
3267
3273
  if (this.busy) return;
3268
3274
  this.setBusy(true);
@@ -3316,14 +3322,14 @@ Lessons learned (respect these):
3316
3322
  async approveAll(runId) {
3317
3323
  const run = this.runs.find((r) => r.id === runId);
3318
3324
  if (!run) return;
3319
- for (const c of run.candidates) {
3320
- if (c.status === "proposed") await this.approve(runId, c.id);
3325
+ for (const c2 of run.candidates) {
3326
+ if (c2.status === "proposed") await this.approve(runId, c2.id);
3321
3327
  if (this.cancelRequested) break;
3322
3328
  }
3323
3329
  }
3324
3330
  reject(runId, candidateId) {
3325
3331
  const run = this.runs.find((r) => r.id === runId);
3326
- const candidate = run?.candidates.find((c) => c.id === candidateId);
3332
+ const candidate = run?.candidates.find((c2) => c2.id === candidateId);
3327
3333
  if (!candidate || candidate.status !== "proposed") return;
3328
3334
  candidate.status = "rejected";
3329
3335
  this.broadcastRuns();
@@ -3423,6 +3429,10 @@ Lessons learned (respect these):
3423
3429
  createdAt: now,
3424
3430
  updatedAt: now
3425
3431
  });
3432
+ this.emitGame({
3433
+ type: input.kind === "agent" ? "factory.agent" : "factory.skill",
3434
+ meta: { name: input.name }
3435
+ });
3426
3436
  }
3427
3437
  for (const a of this.state.artifacts) {
3428
3438
  if (related.includes(a.name) && !a.relatedArtifacts.includes(input.name)) {
@@ -3590,10 +3600,10 @@ function restoreRuns(runs) {
3590
3600
  run.finishedAt = run.finishedAt ?? Date.now();
3591
3601
  run.summary = run.summary || "Interrupted — the app was closed mid-scan.";
3592
3602
  }
3593
- for (const c of run.candidates) {
3594
- if (c.status === "authoring") {
3595
- c.status = "proposed";
3596
- c.result = void 0;
3603
+ for (const c2 of run.candidates) {
3604
+ if (c2.status === "authoring") {
3605
+ c2.status = "proposed";
3606
+ c2.result = void 0;
3597
3607
  }
3598
3608
  }
3599
3609
  }
@@ -3626,9 +3636,11 @@ Feature: ${feature.title}
3626
3636
  Read that spec file in full, then implement every spec it lists. Make your changes in this worktree and commit them as each part works. If any spec is ambiguous, ask before guessing.`;
3627
3637
  }
3628
3638
  class FeatureService {
3629
- constructor(persistence2, sessions) {
3639
+ constructor(persistence2, sessions, emitGame = () => {
3640
+ }) {
3630
3641
  this.persistence = persistence2;
3631
3642
  this.sessions = sessions;
3643
+ this.emitGame = emitGame;
3632
3644
  }
3633
3645
  get features() {
3634
3646
  return this.persistence.state.features;
@@ -3649,8 +3661,12 @@ class FeatureService {
3649
3661
  save(feature) {
3650
3662
  const list = this.features;
3651
3663
  const idx = list.findIndex((f) => f.id === feature.id);
3652
- if (idx >= 0) list[idx] = feature;
3653
- else list.push(feature);
3664
+ if (idx >= 0) {
3665
+ list[idx] = feature;
3666
+ } else {
3667
+ list.push(feature);
3668
+ this.emitGame({ type: "feature.save" });
3669
+ }
3654
3670
  this.persistence.scheduleSave();
3655
3671
  }
3656
3672
  delete(id) {
@@ -3842,6 +3858,310 @@ class FsService {
3842
3858
  }
3843
3859
  }
3844
3860
  }
3861
+ const XP_TABLE = {
3862
+ "session.turn": 5,
3863
+ "conductor.turn": 5,
3864
+ "action.run": 8,
3865
+ "checkpoint.create": 10,
3866
+ "sentinel.run": 12,
3867
+ "session.create": 20,
3868
+ "worktree.create": 25,
3869
+ "feature.save": 30,
3870
+ "autoexpand.done": 40,
3871
+ "worktree.pr": 50,
3872
+ "worktree.merge": 60,
3873
+ "factory.skill": 75,
3874
+ "factory.agent": 75,
3875
+ "feature.merge": 80
3876
+ };
3877
+ function xpForEvent(e) {
3878
+ let xp = XP_TABLE[e.type] ?? 0;
3879
+ if (e.type === "worktree.merge") xp += Math.min(e.meta?.commits ?? 0, 10) * 3;
3880
+ return xp;
3881
+ }
3882
+ function xpForLevel(n) {
3883
+ if (n <= 1) return 0;
3884
+ const k = n - 1;
3885
+ return 50 * k * k + 50 * k;
3886
+ }
3887
+ function levelForXp(xp) {
3888
+ if (xp <= 0) return 1;
3889
+ return Math.max(1, Math.floor((-50 + Math.sqrt(2500 + 200 * xp)) / 100) + 1);
3890
+ }
3891
+ function levelInfo(xp) {
3892
+ const level = levelForXp(xp);
3893
+ const floor = xpForLevel(level);
3894
+ const next = xpForLevel(level + 1);
3895
+ return { level, xpIntoLevel: xp - floor, xpForNextLevel: next - floor };
3896
+ }
3897
+ const DEFAULT_GAME_STATE = {
3898
+ xp: 0,
3899
+ streak: { current: 0, longest: 0, lastDay: "" },
3900
+ achievements: {},
3901
+ todaysQuests: [],
3902
+ questDay: "",
3903
+ counters: {},
3904
+ nightTurns: 0,
3905
+ earlyTurns: 0,
3906
+ createdAt: 0
3907
+ };
3908
+ const c = (ctx, t) => ctx.counters[t] ?? 0;
3909
+ const ACHIEVEMENTS = [
3910
+ // sessions
3911
+ { id: "first-session", title: "Hello, Maestro", desc: "Start your first session.", icon: "🎬", category: "sessions", predicate: (x) => c(x, "session.create") >= 1 },
3912
+ { id: "ten-sessions", title: "Regular", desc: "Start 10 sessions.", icon: "📁", category: "sessions", predicate: (x) => c(x, "session.create") >= 10 },
3913
+ { id: "worktree-novice", title: "Branching Out", desc: "Create your first parallel task.", icon: "🌱", category: "sessions", predicate: (x) => c(x, "worktree.create") >= 1 },
3914
+ { id: "worktree-adept", title: "Multitasker", desc: "Create 10 parallel tasks.", icon: "🌳", category: "sessions", predicate: (x) => c(x, "worktree.create") >= 10 },
3915
+ // merges
3916
+ { id: "first-merge", title: "Merge One", desc: "Merge your first worktree.", icon: "🔀", category: "merges", predicate: (x) => c(x, "worktree.merge") >= 1 },
3917
+ { id: "ten-merges", title: "Merge Maestro", desc: "Merge 10 worktrees.", icon: "🧬", category: "merges", predicate: (x) => c(x, "worktree.merge") >= 10 },
3918
+ { id: "fifty-merges", title: "Merge Machine", desc: "Merge 50 worktrees.", icon: "⚙️", category: "merges", predicate: (x) => c(x, "worktree.merge") >= 50 },
3919
+ { id: "pr-opener", title: "Pull Request", desc: "Open your first PR.", icon: "📤", category: "merges", predicate: (x) => c(x, "worktree.pr") >= 1 },
3920
+ { id: "pr-prolific", title: "PR Prolific", desc: "Open 10 PRs.", icon: "🚀", category: "merges", predicate: (x) => c(x, "worktree.pr") >= 10 },
3921
+ { id: "feature-shipper", title: "Ship It", desc: "Merge a feature you specced.", icon: "📦", category: "merges", predicate: (x) => c(x, "feature.merge") >= 1 },
3922
+ // turns
3923
+ { id: "first-turn", title: "First Light", desc: "Finish your first Claude turn.", icon: "✶", category: "turns", predicate: (x) => c(x, "session.turn") >= 1 },
3924
+ { id: "hundred-turns", title: "Century", desc: "Finish 100 turns.", icon: "💯", category: "turns", predicate: (x) => c(x, "session.turn") >= 100 },
3925
+ { id: "thousand-turns", title: "Marathoner", desc: "Finish 1,000 turns.", icon: "🏃", category: "turns", predicate: (x) => c(x, "session.turn") >= 1e3 },
3926
+ { id: "conductor-curious", title: "Conductor Curious", desc: "Have your first Conductor turn.", icon: "✦", category: "turns", predicate: (x) => c(x, "conductor.turn") >= 1 },
3927
+ { id: "conductor-regular", title: "Maestro of Maestro", desc: "Have 50 Conductor turns.", icon: "🎼", category: "turns", predicate: (x) => c(x, "conductor.turn") >= 50 },
3928
+ // factory
3929
+ { id: "first-skill", title: "Skill Smith", desc: "Create your first skill.", icon: "🛠", category: "factory", predicate: (x) => c(x, "factory.skill") >= 1 },
3930
+ { id: "first-agent", title: "Agent Architect", desc: "Create your first agent.", icon: "🤖", category: "factory", predicate: (x) => c(x, "factory.agent") >= 1 },
3931
+ { id: "toolsmith", title: "Toolsmith", desc: "Create 5 skills/agents.", icon: "⚒", category: "factory", predicate: (x) => c(x, "factory.skill") + c(x, "factory.agent") >= 5 },
3932
+ { id: "master-toolsmith", title: "Master Toolsmith", desc: "Create 20 skills/agents.", icon: "🏭", category: "factory", predicate: (x) => c(x, "factory.skill") + c(x, "factory.agent") >= 20 },
3933
+ // streak
3934
+ { id: "streak-3", title: "Warmed Up", desc: "Keep a 3-day streak.", icon: "🔥", category: "streak", predicate: (x) => x.streakLongest >= 3 },
3935
+ { id: "streak-7", title: "On Fire", desc: "Keep a 7-day streak.", icon: "🔥", category: "streak", predicate: (x) => x.streakLongest >= 7 },
3936
+ { id: "streak-30", title: "Unstoppable", desc: "Keep a 30-day streak.", icon: "☄️", category: "streak", predicate: (x) => x.streakLongest >= 30 },
3937
+ // time of day
3938
+ { id: "night-owl", title: "Night Owl", desc: "Finish a turn between midnight and 5am.", icon: "🦉", category: "time", predicate: (x) => x.nightTurns >= 1 },
3939
+ { id: "early-bird", title: "Early Bird", desc: "Finish a turn between 5 and 8am.", icon: "🌅", category: "time", predicate: (x) => x.earlyTurns >= 1 },
3940
+ // level
3941
+ { id: "level-10", title: "Double Digits", desc: "Reach level 10.", icon: "⭐", category: "level", predicate: (x) => x.level >= 10 },
3942
+ { id: "level-25", title: "Maestro Prime", desc: "Reach level 25.", icon: "🌟", category: "level", predicate: (x) => x.level >= 25 }
3943
+ ];
3944
+ const DAILY_QUEST_POOL = [
3945
+ { id: "q-turns-5", title: "Finish 5 Claude turns", target: 5, reward: 30, events: ["session.turn"] },
3946
+ { id: "q-merge-1", title: "Merge a worktree", target: 1, reward: 40, events: ["worktree.merge"] },
3947
+ { id: "q-merge-3", title: "Merge 3 worktrees", target: 3, reward: 100, events: ["worktree.merge"] },
3948
+ { id: "q-create-1", title: "Start a session", target: 1, reward: 20, events: ["session.create"] },
3949
+ { id: "q-worktree-2", title: "Spin up 2 parallel tasks", target: 2, reward: 50, events: ["worktree.create"] },
3950
+ { id: "q-checkpoint", title: "Make a checkpoint", target: 1, reward: 20, events: ["checkpoint.create"] },
3951
+ { id: "q-conductor-3", title: "Have 3 Conductor turns", target: 3, reward: 30, events: ["conductor.turn"] },
3952
+ { id: "q-factory-1", title: "Create a skill or agent", target: 1, reward: 75, events: ["factory.skill", "factory.agent"] },
3953
+ { id: "q-feature-1", title: "Save a feature spec", target: 1, reward: 30, events: ["feature.save"] },
3954
+ { id: "q-pr-1", title: "Open a pull request", target: 1, reward: 50, events: ["worktree.pr"] }
3955
+ ];
3956
+ const questDef = (id) => DAILY_QUEST_POOL.find((q) => q.id === id);
3957
+ function hashStr(s) {
3958
+ let h = 2166136261;
3959
+ for (let i = 0; i < s.length; i++) {
3960
+ h ^= s.charCodeAt(i);
3961
+ h = Math.imul(h, 16777619);
3962
+ }
3963
+ return h >>> 0;
3964
+ }
3965
+ function mulberry32(seed) {
3966
+ let a = seed >>> 0;
3967
+ return () => {
3968
+ a = a + 1831565813 | 0;
3969
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
3970
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
3971
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
3972
+ };
3973
+ }
3974
+ function pickDailyQuests(dayKey2, n = 3) {
3975
+ const rnd = mulberry32(hashStr("quest:" + dayKey2));
3976
+ const pool = [...DAILY_QUEST_POOL];
3977
+ for (let i = pool.length - 1; i > 0; i--) {
3978
+ const j = Math.floor(rnd() * (i + 1));
3979
+ [pool[i], pool[j]] = [pool[j], pool[i]];
3980
+ }
3981
+ return pool.slice(0, Math.min(n, pool.length)).map((q) => ({
3982
+ id: q.id,
3983
+ target: q.target,
3984
+ progress: 0,
3985
+ rewarded: false
3986
+ }));
3987
+ }
3988
+ function dayKey(d) {
3989
+ return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
3990
+ }
3991
+ function prevDayKey(d) {
3992
+ const y = new Date(d.getFullYear(), d.getMonth(), d.getDate() - 1);
3993
+ return dayKey(y);
3994
+ }
3995
+ class GamificationStore {
3996
+ file = path.join(electron.app.getPath("userData"), "gamification.json");
3997
+ timer = null;
3998
+ state = structuredClone(DEFAULT_GAME_STATE);
3999
+ /** Load saved progress (best-effort; defaults on any error, back-compat via spread). */
4000
+ load() {
4001
+ try {
4002
+ const raw = JSON.parse(fs.readFileSync(this.file, "utf8"));
4003
+ const s = { ...structuredClone(DEFAULT_GAME_STATE), ...raw ?? {} };
4004
+ const num = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : Number.isFinite(Number(v)) ? Number(v) : fallback;
4005
+ if (!s.streak || typeof s.streak !== "object") s.streak = { current: 0, longest: 0, lastDay: "" };
4006
+ s.streak.current = num(s.streak.current, 0);
4007
+ s.streak.longest = num(s.streak.longest, 0);
4008
+ if (typeof s.streak.lastDay !== "string") s.streak.lastDay = "";
4009
+ if (!s.achievements || typeof s.achievements !== "object" || Array.isArray(s.achievements)) {
4010
+ s.achievements = {};
4011
+ }
4012
+ if (!Array.isArray(s.todaysQuests)) s.todaysQuests = [];
4013
+ s.todaysQuests = s.todaysQuests.filter((q) => q && typeof q.id === "string").map((q) => ({ id: q.id, target: num(q.target, 1), progress: num(q.progress, 0), rewarded: !!q.rewarded }));
4014
+ if (!s.counters || typeof s.counters !== "object" || Array.isArray(s.counters)) s.counters = {};
4015
+ for (const k of Object.keys(s.counters)) {
4016
+ s.counters[k] = num(s.counters[k], 0);
4017
+ }
4018
+ s.nightTurns = num(s.nightTurns, 0);
4019
+ s.earlyTurns = num(s.earlyTurns, 0);
4020
+ s.xp = num(s.xp, 0);
4021
+ if (!s.createdAt) s.createdAt = Date.now();
4022
+ this.state = s;
4023
+ } catch {
4024
+ this.state = structuredClone(DEFAULT_GAME_STATE);
4025
+ this.state.createdAt = Date.now();
4026
+ }
4027
+ return this.state;
4028
+ }
4029
+ get() {
4030
+ return this.state;
4031
+ }
4032
+ set(state) {
4033
+ this.state = state;
4034
+ this.scheduleSave();
4035
+ }
4036
+ scheduleSave() {
4037
+ if (this.timer) clearTimeout(this.timer);
4038
+ this.timer = setTimeout(() => this.saveNow(), 500);
4039
+ }
4040
+ saveNow() {
4041
+ if (this.timer) {
4042
+ clearTimeout(this.timer);
4043
+ this.timer = null;
4044
+ }
4045
+ try {
4046
+ fs.mkdirSync(path.dirname(this.file), { recursive: true });
4047
+ const tmp = this.file + ".tmp";
4048
+ fs.writeFileSync(tmp, JSON.stringify(this.state, null, 2), "utf8");
4049
+ fs.renameSync(tmp, this.file);
4050
+ } catch (err) {
4051
+ console.error("Failed to persist gamification state:", err);
4052
+ }
4053
+ }
4054
+ }
4055
+ const ACHIEVEMENT_XP = 25;
4056
+ class GamificationService {
4057
+ constructor(getWin2) {
4058
+ this.getWin = getWin2;
4059
+ this.state = this.store.load();
4060
+ }
4061
+ store = new GamificationStore();
4062
+ state;
4063
+ /** GameState + derived level fields (for the initial renderer fetch). */
4064
+ snapshot() {
4065
+ return { ...this.state, ...levelInfo(this.state.xp) };
4066
+ }
4067
+ dispose() {
4068
+ this.store.saveNow();
4069
+ }
4070
+ /**
4071
+ * Apply one event: bump counters, roll the day (streak + quests), add XP,
4072
+ * advance quests, unlock achievements — each guarded so nothing double-counts.
4073
+ * Wrapped so a gamification failure can never break the caller's turn/merge.
4074
+ */
4075
+ award(e) {
4076
+ try {
4077
+ const s = this.state;
4078
+ const now = /* @__PURE__ */ new Date();
4079
+ const today = dayKey(now);
4080
+ const celebrations = [];
4081
+ if (s.streak.lastDay !== today) {
4082
+ s.streak.current = s.streak.lastDay === prevDayKey(now) ? s.streak.current + 1 : 1;
4083
+ s.streak.longest = Math.max(s.streak.longest, s.streak.current);
4084
+ s.streak.lastDay = today;
4085
+ s.todaysQuests = pickDailyQuests(today);
4086
+ s.questDay = today;
4087
+ if (s.streak.current >= 2) {
4088
+ celebrations.push({
4089
+ kind: "streak",
4090
+ seed: `streak:${today}:${s.streak.current}`,
4091
+ current: s.streak.current
4092
+ });
4093
+ }
4094
+ }
4095
+ s.counters[e.type] = (s.counters[e.type] ?? 0) + 1;
4096
+ if (e.type === "session.turn" || e.type === "conductor.turn") {
4097
+ const hour = e.meta?.hour ?? now.getHours();
4098
+ if (hour >= 0 && hour < 5) s.nightTurns += 1;
4099
+ else if (hour >= 5 && hour < 8) s.earlyTurns += 1;
4100
+ }
4101
+ const beforeLevel = levelForXp(s.xp);
4102
+ s.xp += xpForEvent(e);
4103
+ for (const q of s.todaysQuests) {
4104
+ if (q.rewarded) continue;
4105
+ const def = questDef(q.id);
4106
+ if (!def || !def.events.includes(e.type)) continue;
4107
+ q.progress = Math.min(q.target, q.progress + 1);
4108
+ if (q.progress >= q.target) {
4109
+ q.rewarded = true;
4110
+ s.xp += def.reward;
4111
+ celebrations.push({
4112
+ kind: "quest",
4113
+ seed: `quest:${today}:${q.id}`,
4114
+ id: q.id,
4115
+ title: def.title,
4116
+ xp: def.reward
4117
+ });
4118
+ }
4119
+ }
4120
+ let unlockedThisPass = true;
4121
+ while (unlockedThisPass) {
4122
+ unlockedThisPass = false;
4123
+ const ctx = {
4124
+ counters: s.counters,
4125
+ nightTurns: s.nightTurns,
4126
+ earlyTurns: s.earlyTurns,
4127
+ streakLongest: s.streak.longest,
4128
+ level: levelForXp(s.xp)
4129
+ };
4130
+ for (const a of ACHIEVEMENTS) {
4131
+ if (s.achievements[a.id]) continue;
4132
+ if (a.predicate(ctx)) {
4133
+ s.achievements[a.id] = { unlockedAt: Date.now() };
4134
+ s.xp += ACHIEVEMENT_XP;
4135
+ unlockedThisPass = true;
4136
+ celebrations.push({
4137
+ kind: "achievement",
4138
+ seed: `ach:${a.id}`,
4139
+ id: a.id,
4140
+ title: a.title,
4141
+ icon: a.icon,
4142
+ xp: ACHIEVEMENT_XP
4143
+ });
4144
+ }
4145
+ }
4146
+ }
4147
+ const afterLevel = levelForXp(s.xp);
4148
+ for (let l = beforeLevel + 1; l <= afterLevel; l++) {
4149
+ celebrations.push({ kind: "level-up", seed: `level:${l}`, level: l });
4150
+ }
4151
+ this.store.set(s);
4152
+ this.broadcast();
4153
+ for (const c2 of celebrations) this.celebrate(c2);
4154
+ } catch (err) {
4155
+ console.error("Gamification award failed (ignored):", err);
4156
+ }
4157
+ }
4158
+ broadcast() {
4159
+ this.getWin()?.webContents.send("gamification:changed", this.snapshot());
4160
+ }
4161
+ celebrate(c2) {
4162
+ this.getWin()?.webContents.send("gamification:celebrate", c2);
4163
+ }
4164
+ }
3845
4165
  const IMAGE_EXTS$1 = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"]);
3846
4166
  const THUMB_HEIGHT = 64;
3847
4167
  const MAX_LISTED = 100;
@@ -4256,7 +4576,7 @@ function tokenize(template) {
4256
4576
  while (m = re.exec(template)) tokens.push(m[1] ?? m[2]);
4257
4577
  return tokens;
4258
4578
  }
4259
- function registerIpc(sessions, fs2, persistence2, sentinels, features, autoExpand, conductor, factory, agents, tokenEff, getWin2) {
4579
+ function registerIpc(sessions, fs2, persistence2, sentinels, features, autoExpand, conductor, factory, agents, tokenEff, gamification, getWin2) {
4260
4580
  const rootOf = (id) => {
4261
4581
  const config = sessions.getConfig(id);
4262
4582
  if (!config) throw new Error(`Unknown session: ${id}`);
@@ -4469,6 +4789,7 @@ function registerIpc(sessions, fs2, persistence2, sentinels, features, autoExpan
4469
4789
  (_e, id, kind) => factory.createFromSuggestion(id, kind)
4470
4790
  );
4471
4791
  electron.ipcMain.handle("factory:dismissSuggestion", (_e, id) => factory.dismissSuggestion(id));
4792
+ electron.ipcMain.handle("gamification:get", () => gamification.snapshot());
4472
4793
  electron.ipcMain.handle("agents:get", () => agents.snapshot());
4473
4794
  electron.ipcMain.handle("agents:refresh", () => agents.refresh());
4474
4795
  electron.ipcMain.handle("agents:read", (_e, filePath) => agents.readAgentFile(filePath));
@@ -4656,7 +4977,12 @@ const DEFAULT_SETTINGS = {
4656
4977
  watchdogUnansweredMinutes: 5,
4657
4978
  tokenEfficiency: DEFAULT_TOKEN_EFFICIENCY,
4658
4979
  tokenEfficiencyRepoOverrides: {},
4659
- agentRegistryPath: "C:\\repos\\agent-factory\\registry\\registry.json"
4980
+ agentRegistryPath: "C:\\repos\\agent-factory\\registry\\registry.json",
4981
+ theme: "dark",
4982
+ accentColor: null,
4983
+ gamificationEnabled: true,
4984
+ gamificationReduceMotion: false,
4985
+ gamificationSound: false
4660
4986
  };
4661
4987
  const DEFAULT_CATEGORIES = [
4662
4988
  {
@@ -4826,9 +5152,11 @@ function gitHead$1(folder) {
4826
5152
  });
4827
5153
  }
4828
5154
  class SentinelService {
4829
- constructor(persistence2, getWin2) {
5155
+ constructor(persistence2, getWin2, emitGame = () => {
5156
+ }) {
4830
5157
  this.persistence = persistence2;
4831
5158
  this.getWin = getWin2;
5159
+ this.emitGame = emitGame;
4832
5160
  }
4833
5161
  timer = null;
4834
5162
  /** Last observed HEAD per session id; baseline is set without firing. */
@@ -5008,6 +5336,7 @@ class SentinelService {
5008
5336
  run.summary = summary;
5009
5337
  run.findings = findings;
5010
5338
  this.broadcast(run.sessionId);
5339
+ if (status !== "error") this.emitGame({ type: "sentinel.run" });
5011
5340
  }
5012
5341
  broadcast(sessionId) {
5013
5342
  this.getWin()?.webContents.send("sentinel:runs", sessionId, this.listRuns(sessionId));
@@ -5287,11 +5616,13 @@ const STATUS_PRIORITY = [
5287
5616
  "exited"
5288
5617
  ];
5289
5618
  class SessionManager {
5290
- constructor(persistence2, fs2, tokenEff, getWin2) {
5619
+ constructor(persistence2, fs2, tokenEff, getWin2, emitGame = () => {
5620
+ }) {
5291
5621
  this.persistence = persistence2;
5292
5622
  this.fs = fs2;
5293
5623
  this.tokenEff = tokenEff;
5294
5624
  this.getWin = getWin2;
5625
+ this.emitGame = emitGame;
5295
5626
  }
5296
5627
  /** Keyed by terminal id, across all sessions. */
5297
5628
  ptys = /* @__PURE__ */ new Map();
@@ -5316,6 +5647,8 @@ class SessionManager {
5316
5647
  watchdog = /* @__PURE__ */ new Map();
5317
5648
  /** The watchdog interval; null until startWatchdog() runs. */
5318
5649
  watchdogTimer = null;
5650
+ /** Per-terminal last status, used only to award a turn on the working→done edge. */
5651
+ lastStatusForAward = /* @__PURE__ */ new Map();
5319
5652
  get state() {
5320
5653
  return this.persistence.state;
5321
5654
  }
@@ -5327,12 +5660,12 @@ class SessionManager {
5327
5660
  }
5328
5661
  categoryOf(config) {
5329
5662
  if (!config.categoryId) return null;
5330
- return this.state.categories.find((c) => c.id === config.categoryId) ?? null;
5663
+ return this.state.categories.find((c2) => c2.id === config.categoryId) ?? null;
5331
5664
  }
5332
5665
  /** Every MCP server name owned by any category — our materialization namespace. */
5333
5666
  managedServerNames() {
5334
5667
  const names = /* @__PURE__ */ new Set();
5335
- for (const c of this.state.categories) for (const s of c.mcpServers) names.add(s.name);
5668
+ for (const c2 of this.state.categories) for (const s of c2.mcpServers) names.add(s.name);
5336
5669
  return [...names];
5337
5670
  }
5338
5671
  /**
@@ -5383,6 +5716,7 @@ class SessionManager {
5383
5716
  for (const terminal of config.terminals) this.spawnTerminal(config, terminal, "fresh");
5384
5717
  this.fs.start(config.id, config.folder, []);
5385
5718
  this.notifyChanged();
5719
+ this.emitGame({ type: "session.create" });
5386
5720
  return this.toInfo(config);
5387
5721
  }
5388
5722
  close(id) {
@@ -5472,7 +5806,9 @@ class SessionManager {
5472
5806
  async createCheckpoint(sessionId, label) {
5473
5807
  const config = this.getConfig(sessionId);
5474
5808
  if (!config) throw new Error("Unknown session");
5475
- return createCheckpoint(config.folder, label);
5809
+ const checkpoint = await createCheckpoint(config.folder, label);
5810
+ this.emitGame({ type: "checkpoint.create" });
5811
+ return checkpoint;
5476
5812
  }
5477
5813
  /** Recent checkpoints for a session's repo, newest first. */
5478
5814
  async listCheckpoints(sessionId) {
@@ -5580,6 +5916,7 @@ class SessionManager {
5580
5916
  }, INITIAL_PROMPT_DELAY_MS);
5581
5917
  }
5582
5918
  this.notifyChanged();
5919
+ this.emitGame({ type: "worktree.create" });
5583
5920
  return this.toInfo(config);
5584
5921
  }
5585
5922
  /** Live git facts about a worktree task (uncommitted files, commits ahead). */
@@ -5661,7 +5998,12 @@ ${commit.output}` };
5661
5998
  }
5662
5999
  const result = await mergeBranch(baseFolder, branch, baseBranch);
5663
6000
  if (!result.ok) return { ...result, autoCommitted };
5664
- return { ...await this.pushAfterMerge(result, baseFolder, baseBranch), autoCommitted };
6001
+ const merged = { ...await this.pushAfterMerge(result, baseFolder, baseBranch), autoCommitted };
6002
+ this.emitGame({ type: "worktree.merge", meta: { commits: ahead ?? 0 } });
6003
+ if (this.state.features.some((f) => f.taskSessionId === sessionId)) {
6004
+ this.emitGame({ type: "feature.merge" });
6005
+ }
6006
+ return merged;
5665
6007
  }
5666
6008
  /** True if `branch` is the dedicated expansion branch of any auto-expand config. */
5667
6009
  isAutoExpandBranch(branch) {
@@ -5756,6 +6098,7 @@ ${commit.output}` };
5756
6098
  const title = prTitle(config.name, branch);
5757
6099
  const body = prBody(config.name, branch, baseBranch);
5758
6100
  const result = await createPullRequest(config.folder, branch, baseBranch, title, body);
6101
+ if (result.ok) this.emitGame({ type: "worktree.pr" });
5759
6102
  return { ...result, autoCommitted };
5760
6103
  }
5761
6104
  // ---------- auto-complete (auto-merge / auto-PR when claude finishes) --------
@@ -6040,6 +6383,7 @@ ${err.message}`
6040
6383
  const action = this.state.actions.find((a) => a.id === actionId);
6041
6384
  const command = action?.command.trim();
6042
6385
  if (!config || !action || !command) return null;
6386
+ this.emitGame({ type: "action.run" });
6043
6387
  if (action.shell === "claude") return this.runClaudeAction(config, command);
6044
6388
  let terminal = config.terminals.find((t) => t.actionId === action.id);
6045
6389
  let respawned = false;
@@ -6282,6 +6626,11 @@ ${err.message}`
6282
6626
  if (!config) return;
6283
6627
  const terminal = config.terminals.find((t) => t.id === terminalId);
6284
6628
  if (!terminal || terminal.kind !== "claude") return;
6629
+ const prevForAward = this.lastStatusForAward.get(terminalId);
6630
+ this.lastStatusForAward.set(terminalId, status);
6631
+ if (status === "done" && prevForAward === "working") {
6632
+ this.emitGame({ type: "session.turn" });
6633
+ }
6285
6634
  if (status === "done" || status === "idle") this.scheduleQueueDispatch(config.id);
6286
6635
  if (config.worktree?.autoComplete) {
6287
6636
  if (status === "working") this.worktreeWorked.add(config.id);
@@ -7286,18 +7635,27 @@ if (!gotLock) {
7286
7635
  electron.app.whenReady().then(() => {
7287
7636
  electron.app.setAppUserModelId("com.pedroferreira.maestro");
7288
7637
  persistence.load();
7638
+ const gameBus = new events.EventEmitter();
7639
+ const emitGame = (e) => {
7640
+ try {
7641
+ gameBus.emit("game", e);
7642
+ } catch {
7643
+ }
7644
+ };
7289
7645
  const fsService = new FsService(
7290
- (sessionId, events) => getWin()?.webContents.send("fs:events", sessionId, events),
7646
+ (sessionId, events2) => getWin()?.webContents.send("fs:events", sessionId, events2),
7291
7647
  () => persistence.state.settings.ignoreNames
7292
7648
  );
7293
7649
  const tokenEff = new TokenEfficiencyService(persistence);
7294
- const sessions = new SessionManager(persistence, fsService, tokenEff, getWin);
7295
- const sentinels = new SentinelService(persistence, getWin);
7296
- const features = new FeatureService(persistence, sessions);
7297
- const autoExpand = new AutoExpandService(persistence, features, getWin);
7650
+ const sessions = new SessionManager(persistence, fsService, tokenEff, getWin, emitGame);
7651
+ const sentinels = new SentinelService(persistence, getWin, emitGame);
7652
+ const features = new FeatureService(persistence, sessions, emitGame);
7653
+ const autoExpand = new AutoExpandService(persistence, features, getWin, emitGame);
7298
7654
  const conductor = new ConductorService(persistence, sessions, features, autoExpand, getWin);
7299
- const factory = new FactoryService(getWin);
7655
+ const factory = new FactoryService(getWin, emitGame);
7300
7656
  const agentRegistry = new AgentRegistryService(persistence, getWin);
7657
+ const gamification = new GamificationService(getWin);
7658
+ gameBus.on("game", (e) => gamification.award(e));
7301
7659
  registerIpc(
7302
7660
  sessions,
7303
7661
  fsService,
@@ -7309,6 +7667,7 @@ if (!gotLock) {
7309
7667
  factory,
7310
7668
  agentRegistry,
7311
7669
  tokenEff,
7670
+ gamification,
7312
7671
  getWin
7313
7672
  );
7314
7673
  createWindow();
@@ -7318,13 +7677,17 @@ if (!gotLock) {
7318
7677
  autoExpand.start();
7319
7678
  tokenEff.start();
7320
7679
  factory.start();
7321
- conductor.onTurnComplete((messages) => factory.considerConversation(messages));
7680
+ conductor.onTurnComplete((messages) => {
7681
+ factory.considerConversation(messages);
7682
+ emitGame({ type: "conductor.turn" });
7683
+ });
7322
7684
  electron.app.on("activate", () => {
7323
7685
  if (electron.BrowserWindow.getAllWindows().length === 0) createWindow();
7324
7686
  });
7325
7687
  electron.app.on("before-quit", () => {
7326
7688
  tokenEff.dispose();
7327
7689
  agentRegistry.dispose();
7690
+ gamification.dispose();
7328
7691
  factory.dispose();
7329
7692
  conductor.dispose();
7330
7693
  autoExpand.dispose();