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.
- package/out/main/index.js +419 -56
- package/out/preload/index.js +3 -0
- package/out/renderer/assets/{index-CNNAMsV1.js → index--LsdCcT2.js} +2 -2
- package/out/renderer/assets/{index-Dhxn3JIv.js → index-2Q5NJLLa.js} +2 -2
- package/out/renderer/assets/{index-Dgaj6c_K.css → index-B4pxYlCv.css} +442 -2
- package/out/renderer/assets/{index-B59uuZRU.js → index-B9wQ40iJ.js} +4 -4
- package/out/renderer/assets/{index-DI2ly48w.js → index-BD5wVgZG.js} +2 -2
- package/out/renderer/assets/{index-CWk6CwGd.js → index-BKVqTAbd.js} +3 -3
- package/out/renderer/assets/{index-JMVyecfQ.js → index-BSnEdUx8.js} +5 -5
- package/out/renderer/assets/{index-C479DZmL.js → index-BUr9qTcP.js} +5 -5
- package/out/renderer/assets/{index-C0rsWi9C.js → index-BX4eMUiW.js} +2 -2
- package/out/renderer/assets/{index-CuHjzw7d.js → index-BYrBOYyo.js} +5 -5
- package/out/renderer/assets/{index-D9GPva9-.js → index-BdkQGOfF.js} +5 -5
- package/out/renderer/assets/{index-CTxGDYbk.js → index-BeXyvqqV.js} +2026 -1461
- package/out/renderer/assets/{index-9AHdXE8U.js → index-C2xG1-2F.js} +2 -2
- package/out/renderer/assets/{index-CVWvgy2Y.js → index-CALj_g-h.js} +2 -2
- package/out/renderer/assets/{index-Bg4ondS2.js → index-CqEbh7gN.js} +2 -2
- package/out/renderer/assets/{index-1Z03T0zz.js → index-CwrdXZxY.js} +2 -2
- package/out/renderer/assets/{index-jAA5WJm3.js → index-DFnn9t0U.js} +5 -5
- package/out/renderer/assets/{index-CZP8wVw-.js → index-DMLpIsZn.js} +2 -2
- package/out/renderer/assets/{index-CoyUYEik.js → index-Dpj9EP42.js} +3 -3
- package/out/renderer/assets/{index-Cq5xQaOf.js → index-DwCelZNB.js} +5 -5
- package/out/renderer/assets/{index-BkOzhsuz.js → index-K7jPnu8A.js} +1 -1
- package/out/renderer/assets/{index-LW-gCnC-.js → index-KjJOaoK3.js} +2 -2
- package/out/renderer/assets/{index-DJwKAmOm.js → index-NXP0WfIH.js} +2 -2
- package/out/renderer/assets/{index-CXeHg_Qc.js → index-RU5VUPJx.js} +2 -2
- package/out/renderer/index.html +2 -2
- 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((
|
|
1013
|
-
const cmd = candidates.find((
|
|
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
|
|
2025
|
-
const git2 = await this.sessions.getGitStatus(
|
|
2026
|
-
const feats = this.features.list(
|
|
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:
|
|
2035
|
-
name:
|
|
2036
|
-
folder:
|
|
2037
|
-
isActive:
|
|
2038
|
+
id: c2.id,
|
|
2039
|
+
name: c2.name,
|
|
2040
|
+
folder: c2.folder,
|
|
2041
|
+
isActive: c2.id === activeId,
|
|
2038
2042
|
status: s.status,
|
|
2039
|
-
categoryId:
|
|
2043
|
+
categoryId: c2.categoryId ?? null,
|
|
2040
2044
|
terminals: s.terminals.map((t) => ({ kind: t.config.kind, status: t.status })),
|
|
2041
|
-
worktree:
|
|
2042
|
-
parentSessionId:
|
|
2043
|
-
branch:
|
|
2044
|
-
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:
|
|
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 (
|
|
2056
|
-
const task = await this.sessions.getWorktreeTaskState(
|
|
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
|
|
2627
|
-
if (
|
|
2628
|
-
if (this.suggestionDuplicate(
|
|
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:
|
|
2631
|
-
name:
|
|
2632
|
-
title:
|
|
2633
|
-
description:
|
|
2634
|
-
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:
|
|
2641
|
-
keywords:
|
|
2642
|
-
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((
|
|
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
|
|
3320
|
-
if (
|
|
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((
|
|
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
|
|
3594
|
-
if (
|
|
3595
|
-
|
|
3596
|
-
|
|
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)
|
|
3653
|
-
|
|
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((
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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) =>
|
|
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();
|