@prestyj/cli 5.30.2 → 5.31.0
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/dist/app-sidecar/app-settings.d.ts +6 -2
- package/dist/app-sidecar/app-settings.js +39 -20
- package/dist/app-sidecar/app-settings.js.map +1 -1
- package/dist/app-sidecar/git-target.d.ts +17 -0
- package/dist/app-sidecar/git-target.js +31 -0
- package/dist/app-sidecar/git-target.js.map +1 -0
- package/dist/app-sidecar/goal-routes.d.ts +6 -0
- package/dist/app-sidecar/goal-routes.js +360 -0
- package/dist/app-sidecar/goal-routes.js.map +1 -0
- package/dist/app-sidecar/history-routes.d.ts +13 -0
- package/dist/app-sidecar/history-routes.js +37 -27
- package/dist/app-sidecar/history-routes.js.map +1 -1
- package/dist/app-sidecar/mcp-routes.js +1 -2
- package/dist/app-sidecar/mcp-routes.js.map +1 -1
- package/dist/app-sidecar/prompt-routes.js +1 -2
- package/dist/app-sidecar/prompt-routes.js.map +1 -1
- package/dist/app-sidecar/route-context.d.ts +12 -0
- package/dist/app-sidecar/run-control-routes.js +2 -1
- package/dist/app-sidecar/run-control-routes.js.map +1 -1
- package/dist/app-sidecar/session-routes.js +41 -0
- package/dist/app-sidecar/session-routes.js.map +1 -1
- package/dist/app-sidecar/session-types.d.ts +2 -0
- package/dist/app-sidecar/workspace-routes.js +13 -95
- package/dist/app-sidecar/workspace-routes.js.map +1 -1
- package/dist/app-sidecar.js +219 -67
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli/shared.js +1 -1
- package/dist/cli/shared.js.map +1 -1
- package/dist/core/acceptance-checks.js +17 -4
- package/dist/core/acceptance-checks.js.map +1 -1
- package/dist/core/agent-notifications.d.ts +1 -1
- package/dist/core/agent-session.d.ts +90 -5
- package/dist/core/agent-session.js +185 -63
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/ask-user.d.ts +11 -0
- package/dist/core/ask-user.js +6 -1
- package/dist/core/ask-user.js.map +1 -1
- package/dist/core/auth-providers.d.ts +2 -80
- package/dist/core/auth-providers.js +4 -183
- package/dist/core/auth-providers.js.map +1 -1
- package/dist/core/cache-diagnostics.d.ts +1 -1
- package/dist/core/cache-diagnostics.js +2 -1
- package/dist/core/cache-diagnostics.js.map +1 -1
- package/dist/core/compaction/tool-result-pruner.d.ts +10 -0
- package/dist/core/compaction/tool-result-pruner.js +12 -0
- package/dist/core/compaction/tool-result-pruner.js.map +1 -1
- package/dist/core/context-files.d.ts +8 -0
- package/dist/core/context-files.js +46 -0
- package/dist/core/context-files.js.map +1 -0
- package/dist/core/custom-commands.d.ts +6 -0
- package/dist/core/custom-commands.js +17 -0
- package/dist/core/custom-commands.js.map +1 -1
- package/dist/core/env-delta.d.ts +9 -1
- package/dist/core/env-delta.js +20 -0
- package/dist/core/env-delta.js.map +1 -1
- package/dist/core/goal-summary.d.ts +32 -0
- package/dist/core/goal-summary.js +104 -0
- package/dist/core/goal-summary.js.map +1 -0
- package/dist/core/goals-store.d.ts +208 -0
- package/dist/core/goals-store.js +521 -0
- package/dist/core/goals-store.js.map +1 -0
- package/dist/core/local-endpoint-store.js +15 -11
- package/dist/core/local-endpoint-store.js.map +1 -1
- package/dist/core/plan-handoff.d.ts +8 -0
- package/dist/core/plan-handoff.js +11 -0
- package/dist/core/plan-handoff.js.map +1 -1
- package/dist/core/project-discovery.d.ts +7 -0
- package/dist/core/project-discovery.js +9 -3
- package/dist/core/project-discovery.js.map +1 -1
- package/dist/core/radio.d.ts +2 -3
- package/dist/core/radio.js.map +1 -1
- package/dist/core/session-manager.d.ts +7 -1
- package/dist/core/session-manager.js +17 -7
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-storage.d.ts +3 -0
- package/dist/core/session-storage.js +8 -2
- package/dist/core/session-storage.js.map +1 -1
- package/dist/core/steroids.d.ts +8 -0
- package/dist/core/steroids.js +19 -3
- package/dist/core/steroids.js.map +1 -1
- package/dist/core/subagent-manager.d.ts +51 -0
- package/dist/core/subagent-manager.js +103 -16
- package/dist/core/subagent-manager.js.map +1 -1
- package/dist/core/subagent-store.js +2 -1
- package/dist/core/subagent-store.js.map +1 -1
- package/dist/goals-agent/goal-team.d.ts +2 -0
- package/dist/goals-agent/goal-team.js +83 -0
- package/dist/goals-agent/goal-team.js.map +1 -0
- package/dist/goals-agent/goals-agent.d.ts +90 -0
- package/dist/goals-agent/goals-agent.js +246 -0
- package/dist/goals-agent/goals-agent.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interactive.js +4 -3
- package/dist/interactive.js.map +1 -1
- package/dist/modes/acp-autopilot.d.ts +27 -0
- package/dist/modes/acp-autopilot.js +54 -0
- package/dist/modes/acp-autopilot.js.map +1 -0
- package/dist/modes/acp-goals.d.ts +48 -0
- package/dist/modes/acp-goals.js +103 -0
- package/dist/modes/acp-goals.js.map +1 -0
- package/dist/modes/acp-mode.d.ts +128 -3
- package/dist/modes/acp-mode.js +1082 -52
- package/dist/modes/acp-mode.js.map +1 -1
- package/dist/modes/subagent-worker-mode.d.ts +14 -0
- package/dist/modes/subagent-worker-mode.js +32 -1
- package/dist/modes/subagent-worker-mode.js.map +1 -1
- package/dist/system-prompt.d.ts +8 -0
- package/dist/system-prompt.js +21 -9
- package/dist/system-prompt.js.map +1 -1
- package/dist/tools/bash-read-evidence.d.ts +13 -0
- package/dist/tools/bash-read-evidence.js +48 -1
- package/dist/tools/bash-read-evidence.js.map +1 -1
- package/dist/tools/grep-scan.d.ts +96 -0
- package/dist/tools/grep-scan.js +283 -0
- package/dist/tools/grep-scan.js.map +1 -0
- package/dist/tools/grep.d.ts +10 -0
- package/dist/tools/grep.js +418 -10
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/index.d.ts +7 -1
- package/dist/tools/index.js +10 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/read-tracker.d.ts +6 -12
- package/dist/tools/read-tracker.js +21 -36
- package/dist/tools/read-tracker.js.map +1 -1
- package/dist/tools/read.js +2 -2
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/subagent-control.js +22 -3
- package/dist/tools/subagent-control.js.map +1 -1
- package/dist/tools/write.js +110 -8
- package/dist/tools/write.js.map +1 -1
- package/dist/ui/components/ActivityIndicator.d.ts +5 -5
- package/dist/ui/components/InputArea.d.ts +2 -2
- package/dist/ui/components/InputArea.js +3 -3
- package/dist/ui/components/InputArea.js.map +1 -1
- package/dist/ui/components/ToolExecution.d.ts +2 -2
- package/dist/ui/hooks/useContextCompaction.js +38 -31
- package/dist/ui/hooks/useContextCompaction.js.map +1 -1
- package/dist/utils/format.d.ts +0 -4
- package/dist/utils/format.js +0 -7
- package/dist/utils/format.js.map +1 -1
- package/package.json +4 -4
package/dist/app-sidecar.js
CHANGED
|
@@ -23,7 +23,7 @@ import { setStreamDiagnostic } from "@prestyj/agent";
|
|
|
23
23
|
import { AgentSession } from "./core/agent-session.js";
|
|
24
24
|
import { RunLifecycle } from "./core/run-lifecycle.js";
|
|
25
25
|
import { RunClaim } from "./core/run-claim.js";
|
|
26
|
-
import { shouldOfferPendingPlan } from "./core/plan-handoff.js";
|
|
26
|
+
import { IMPLEMENT_PLAN_PROMPT as SHARED_IMPLEMENT_PLAN_PROMPT, PLAN_SUBMITTED_FOR_REVIEW, shouldOfferPendingPlan, } from "./core/plan-handoff.js";
|
|
27
27
|
import { createChatAgent, parseChatAgentId, switchChatAgent, } from "./chat-agents/index.js";
|
|
28
28
|
import { createMotionAgentSession } from "./motion-agent/motion-agent.js";
|
|
29
29
|
import { buildJiwaTools, JiwaStore } from "./chat-agents/jiwa.js";
|
|
@@ -42,20 +42,18 @@ import { SettingsManager } from "./core/settings-manager.js";
|
|
|
42
42
|
import { getModel, getDefaultThinkingLevel, getContextWindow, clearRuntimeModels, registerRuntimeModels, } from "./core/model-registry.js";
|
|
43
43
|
import { resolveStartOrFallback } from "./core/resolve-start.js";
|
|
44
44
|
import { getGitBranch, getGitDirtyFileCount, isGitRepo } from "./utils/git.js";
|
|
45
|
-
import { getGitHubRepoSlug } from "./utils/github.js";
|
|
46
45
|
import { scoreProjectHealth } from "./core/project-health-score.js";
|
|
47
46
|
import { createRepoPolls } from "./app-sidecar/repo-polls.js";
|
|
48
47
|
import { extractPlanSteps } from "./utils/plan-steps.js";
|
|
49
48
|
import { getSupportedThinkingLevels, isThinkingLevelSupported } from "./core/thinking-level.js";
|
|
50
49
|
import { PROMPT_COMMANDS } from "./core/prompt-commands.js";
|
|
51
|
-
import { loadCustomCommands } from "./core/custom-commands.js";
|
|
52
50
|
import { loadTasksSync, pruneDoneTasksSync } from "./core/task-store.js";
|
|
53
51
|
import { initLogger, log } from "./core/logger.js";
|
|
54
52
|
import { installTerminationHandlers } from "./core/shutdown.js";
|
|
55
53
|
import { stopRadio } from "./core/radio.js";
|
|
56
54
|
import { enrichProcessPath } from "./core/shell-path.js";
|
|
57
55
|
import { createElicitationBridge } from "./core/mcp/index.js";
|
|
58
|
-
import { askSoftDeadlineMs, createAskUserBridge, deliverLateAnswer } from "./core/ask-user.js";
|
|
56
|
+
import { askSoftDeadlineMs, createAskUserBridge, deliverLateAnswer, } from "./core/ask-user.js";
|
|
59
57
|
import { createAskUserTool } from "./tools/ask-user.js";
|
|
60
58
|
import { createUsageService } from "./app-sidecar/usage.js";
|
|
61
59
|
import { daemonReadBody, daemonJson, json } from "./app-sidecar/http.js";
|
|
@@ -65,6 +63,10 @@ import { ALL_PROVIDERS, loadProjectModelPrefs, loadNolanModelPref, loadAutopilot
|
|
|
65
63
|
import { NOLAN_ALLOWED_TOOLS } from "./app-sidecar/nolan-context.js";
|
|
66
64
|
import { createProgressManager } from "./app-sidecar/progress-manager.js";
|
|
67
65
|
import { handleSessionRoutes } from "./app-sidecar/session-routes.js";
|
|
66
|
+
import { handleGoalRoutes } from "./app-sidecar/goal-routes.js";
|
|
67
|
+
import { readGitState, resolveGitCwd } from "./app-sidecar/git-target.js";
|
|
68
|
+
import { goalPhase, goalsRoot, isGoalDir, startGoalOnAnswer, readGoal, } from "./core/goals-store.js";
|
|
69
|
+
import { createGoalsAgentSession, goalCritterPick, loadGoalBriefExtras, } from "./goals-agent/goals-agent.js";
|
|
68
70
|
import { handleWorkspaceRoutes } from "./app-sidecar/workspace-routes.js";
|
|
69
71
|
import { handleHistoryRoutes } from "./app-sidecar/history-routes.js";
|
|
70
72
|
import { handlePromptRoutes } from "./app-sidecar/prompt-routes.js";
|
|
@@ -177,6 +179,18 @@ async function main() {
|
|
|
177
179
|
for (const ctx of sessions.values())
|
|
178
180
|
ctx.broadcast(type, data);
|
|
179
181
|
};
|
|
182
|
+
const sessionIdsAt = (cwd) => {
|
|
183
|
+
const target = path.resolve(cwd);
|
|
184
|
+
return [...sessions.values()]
|
|
185
|
+
.filter((ctx) => path.resolve(ctx.cwd) === target)
|
|
186
|
+
.map((ctx) => ctx.id)
|
|
187
|
+
.sort();
|
|
188
|
+
};
|
|
189
|
+
/** Whether any live window session in `cwd` is working right now. */
|
|
190
|
+
const busyAt = (cwd) => {
|
|
191
|
+
const target = path.resolve(cwd);
|
|
192
|
+
return [...sessions.values()].some((ctx) => path.resolve(ctx.cwd) === target && ctx.isBusy());
|
|
193
|
+
};
|
|
180
194
|
// Providers currently mid-OAuth in some window. Daemon-level so two windows
|
|
181
195
|
// cannot race two browser flows for the same provider into one auth file.
|
|
182
196
|
const oauthInFlightProviders = new Set();
|
|
@@ -271,6 +285,8 @@ async function main() {
|
|
|
271
285
|
broadcastAll,
|
|
272
286
|
oauthInFlightProviders,
|
|
273
287
|
repoPolls,
|
|
288
|
+
sessionIdsAt,
|
|
289
|
+
busyAt,
|
|
274
290
|
}, { id, mode, chatAgent, cwd: sessionCwd, sessionPath });
|
|
275
291
|
sessions.set(id, ctx);
|
|
276
292
|
log("INFO", "app-sidecar", "session created", {
|
|
@@ -279,7 +295,13 @@ async function main() {
|
|
|
279
295
|
chatAgent,
|
|
280
296
|
cwd: sessionCwd,
|
|
281
297
|
});
|
|
282
|
-
|
|
298
|
+
// The conversation file too, so the app can reopen this exact
|
|
299
|
+
// session if the daemon restarts (a new session has no path yet
|
|
300
|
+
// on the app's side).
|
|
301
|
+
daemonJson(res, 200, {
|
|
302
|
+
sessionId: id,
|
|
303
|
+
sessionPath: ctx.session.getState().sessionPath ?? null,
|
|
304
|
+
});
|
|
283
305
|
}
|
|
284
306
|
catch (err) {
|
|
285
307
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -433,7 +455,7 @@ async function main() {
|
|
|
433
455
|
* logger, PATH, shared auth file, and radio live at the daemon level.
|
|
434
456
|
*/
|
|
435
457
|
async function createSession(deps, opts) {
|
|
436
|
-
const { auth, progress, memoryStore, jiwaStore, broadcastAll, oauthInFlightProviders, repoPolls, } = deps;
|
|
458
|
+
const { auth, progress, memoryStore, jiwaStore, broadcastAll, oauthInFlightProviders, repoPolls, sessionIdsAt, busyAt, } = deps;
|
|
437
459
|
const paths = deps.paths;
|
|
438
460
|
const mode = opts.mode;
|
|
439
461
|
let chatAgent = opts.chatAgent;
|
|
@@ -442,6 +464,10 @@ async function createSession(deps, opts) {
|
|
|
442
464
|
// root; create it on first use so a fresh install can start a video at once.
|
|
443
465
|
if (mode === "motion")
|
|
444
466
|
await fs.mkdir(cwd, { recursive: true });
|
|
467
|
+
// A coding session whose cwd is a Goal folder is that goal's coordinator.
|
|
468
|
+
let goal = mode === "code" && isGoalDir(goalsRoot(paths.agentDir), cwd)
|
|
469
|
+
? await readGoal(cwd).then((record) => (record ? { dir: cwd, record } : null))
|
|
470
|
+
: null;
|
|
445
471
|
// Base host for parsing request-URL query params (value is irrelevant to
|
|
446
472
|
// parsing); the daemon owns the real listen host.
|
|
447
473
|
const host = "127.0.0.1";
|
|
@@ -587,7 +613,9 @@ async function createSession(deps, opts) {
|
|
|
587
613
|
onClosed: (ids) => broadcast("ask_user_closed", { ids }),
|
|
588
614
|
onLateAnswer: (late) => {
|
|
589
615
|
log("INFO", "app-sidecar", "ask_user late answer queued", { id: late.prompt.id });
|
|
590
|
-
|
|
616
|
+
// A late "Start the goal now" starts the goal before EZ Goals hears it,
|
|
617
|
+
// so the run it wakes into may already hand out work.
|
|
618
|
+
void startGoalFrom({ action: "answer", answers: late.answers }, late.prompt.questions).then(() => deliverLateAnswer(late, {
|
|
591
619
|
queueMessage: (text) => session.queueMessage(text),
|
|
592
620
|
isBusy: () => running || runClaim.active || autopilotActive,
|
|
593
621
|
onQueued: () => broadcast("queued", {
|
|
@@ -595,13 +623,44 @@ async function createSession(deps, opts) {
|
|
|
595
623
|
messages: session.listQueuedMessages(),
|
|
596
624
|
}),
|
|
597
625
|
startIdleRun: () => void runStrandedQueue(),
|
|
598
|
-
});
|
|
626
|
+
}));
|
|
599
627
|
},
|
|
600
628
|
});
|
|
601
|
-
|
|
629
|
+
// A goal starts when the user picks "Start the goal now" from EZ Goals'
|
|
630
|
+
// question: answered while it waits, or late, after its deadline (see
|
|
631
|
+
// onLateAnswer). Recorded once, and announced so the window can switch to
|
|
632
|
+
// the goal's world view. Never throws: a failed write leaves it briefing.
|
|
633
|
+
const startGoalFrom = async (result, questions) => {
|
|
634
|
+
const current = goal;
|
|
635
|
+
if (!current)
|
|
636
|
+
return;
|
|
637
|
+
const started = await startGoalOnAnswer(current.dir, current.record, result, new Date(), questions).catch((err) => {
|
|
638
|
+
log("WARN", "app-sidecar", "couldn't record goal start", {
|
|
639
|
+
error: err instanceof Error ? err.message : String(err),
|
|
640
|
+
});
|
|
641
|
+
return null;
|
|
642
|
+
});
|
|
643
|
+
if (started) {
|
|
644
|
+
goal = { dir: current.dir, record: started };
|
|
645
|
+
broadcast("goal_state", { id: started.id, phase: goalPhase(started) });
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
const askUserTool = createAskUserTool(async (request) => {
|
|
649
|
+
const result = await asks.park(request);
|
|
650
|
+
await startGoalFrom(result, request.questions);
|
|
651
|
+
return result;
|
|
652
|
+
});
|
|
602
653
|
// The session file path to resume (passed by the daemon's POST /session);
|
|
603
|
-
// empty/unset starts a fresh session.
|
|
604
|
-
|
|
654
|
+
// empty/unset starts a fresh session. A file that's gone (retention cleaned
|
|
655
|
+
// it up, or its goal was deleted) starts fresh too: failing instead left
|
|
656
|
+
// the window with no session at all, so not even its goal list loaded.
|
|
657
|
+
const resumeSessionPath = opts.sessionPath &&
|
|
658
|
+
(await fs.access(opts.sessionPath).then(() => true, () => false))
|
|
659
|
+
? opts.sessionPath
|
|
660
|
+
: undefined;
|
|
661
|
+
if (opts.sessionPath && !resumeSessionPath) {
|
|
662
|
+
log("WARN", "app-sidecar", "session file gone, starting fresh", { cwd });
|
|
663
|
+
}
|
|
605
664
|
let abort = new AbortController();
|
|
606
665
|
const baseSessionOptions = {
|
|
607
666
|
provider,
|
|
@@ -648,6 +707,19 @@ async function createSession(deps, opts) {
|
|
|
648
707
|
additionalTools: [askUserTool],
|
|
649
708
|
});
|
|
650
709
|
}
|
|
710
|
+
else if (goal) {
|
|
711
|
+
// A Goal window talks to EZ Goals, the orchestrator, not the coding agent.
|
|
712
|
+
session = createGoalsAgentSession({
|
|
713
|
+
...baseSessionOptions,
|
|
714
|
+
goal,
|
|
715
|
+
// Memory and the attached projects' rules, read once for this session.
|
|
716
|
+
extras: await loadGoalBriefExtras(goal),
|
|
717
|
+
// Read live: the goal turns active mid-session when the user starts it.
|
|
718
|
+
isActive: () => Boolean(goal?.record.startedAt),
|
|
719
|
+
getChildModel: () => goalCritterPick(goal?.record),
|
|
720
|
+
additionalTools: [askUserTool],
|
|
721
|
+
});
|
|
722
|
+
}
|
|
651
723
|
else {
|
|
652
724
|
session = new AgentSession({
|
|
653
725
|
...baseSessionOptions,
|
|
@@ -681,11 +753,23 @@ async function createSession(deps, opts) {
|
|
|
681
753
|
}
|
|
682
754
|
setPendingPlan(planPath, content);
|
|
683
755
|
broadcast("plan_exit", { planPath, content });
|
|
684
|
-
return
|
|
756
|
+
return PLAN_SUBMITTED_FOR_REVIEW;
|
|
685
757
|
},
|
|
686
758
|
});
|
|
687
759
|
}
|
|
688
760
|
await session.initialize();
|
|
761
|
+
// A goal's attached folders join its workspace so the coordinator can read
|
|
762
|
+
// them. A resumed session may already carry them; `addDirectory` skips those.
|
|
763
|
+
if (goal) {
|
|
764
|
+
for (const item of goal.record.context) {
|
|
765
|
+
if (item.kind !== "folder")
|
|
766
|
+
continue;
|
|
767
|
+
const added = await session.addDirectory(item.path);
|
|
768
|
+
if (!added.ok && !added.error.startsWith("Already in the workspace")) {
|
|
769
|
+
log("WARN", "app-sidecar", "goal context folder unavailable", { error: added.error });
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
689
773
|
if (mode === "chat") {
|
|
690
774
|
const restoredAgent = [...session.getAppMarkers()]
|
|
691
775
|
.reverse()
|
|
@@ -799,20 +883,18 @@ async function createSession(deps, opts) {
|
|
|
799
883
|
});
|
|
800
884
|
}
|
|
801
885
|
// Workspace extras (context window, git status, background tasks). Git state
|
|
802
|
-
// is resolved
|
|
803
|
-
//
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
let
|
|
811
|
-
let gitIsRepo = initialGitIsRepo;
|
|
812
|
-
let gitDirtyFileCount = initialDirtyFileCount;
|
|
886
|
+
// is resolved at startup and refreshed after every run; the context window
|
|
887
|
+
// follows the active model. A Goal window reads git from its attached
|
|
888
|
+
// project folder, not the goal folder (see resolveGitCwd), and re-targets
|
|
889
|
+
// when the goal's folders change (`retargetGit`).
|
|
890
|
+
let gitCwd = await resolveGitCwd(cwd, goal?.record ?? null);
|
|
891
|
+
const initialGit = await readGitState(gitCwd);
|
|
892
|
+
let gitBranch = initialGit.branch;
|
|
893
|
+
let gitIsRepo = initialGit.isRepo;
|
|
894
|
+
let gitDirtyFileCount = initialGit.dirtyFileCount;
|
|
813
895
|
// Open issue/PR counts for the origin repo's GitHub slug, via the `gh` CLI's
|
|
814
896
|
// auth. null = unknown (gh missing/unauthed, non-GitHub origin) → chips hidden.
|
|
815
|
-
|
|
897
|
+
let gitHubSlug = initialGit.gitHubSlug;
|
|
816
898
|
let gitHubIssues = null;
|
|
817
899
|
let gitHubPRs = null;
|
|
818
900
|
let gitHubCI = null;
|
|
@@ -1190,12 +1272,12 @@ async function createSession(deps, opts) {
|
|
|
1190
1272
|
log("INFO", "app-sidecar", "plan handed to user for review", { planPath: plan.planPath });
|
|
1191
1273
|
broadcast("plan_review", plan);
|
|
1192
1274
|
}
|
|
1193
|
-
// Workflow (prompt-template) commands: built-in + the
|
|
1194
|
-
// `.ezcoder/commands/*.md
|
|
1275
|
+
// Workflow (prompt-template) commands: built-in + the custom
|
|
1276
|
+
// `.ezcoder/commands/*.md` of every workspace root (a goal's attached repos too). Used to gate autopilot off command turns and to label
|
|
1195
1277
|
// expanded templates in Nolan's digests. Loaded fresh so a newly added custom
|
|
1196
1278
|
// command is picked up without a restart (mirrors GET /commands).
|
|
1197
1279
|
async function loadWorkflowCommandSpecs() {
|
|
1198
|
-
const custom = await loadCustomCommands(
|
|
1280
|
+
const custom = await session.loadCustomCommands().catch(() => []);
|
|
1199
1281
|
return [
|
|
1200
1282
|
...PROMPT_COMMANDS.map((c) => ({ name: c.name, aliases: c.aliases, prompt: c.prompt })),
|
|
1201
1283
|
...custom.map((c) => ({ name: c.name, aliases: [], prompt: c.prompt })),
|
|
@@ -1444,15 +1526,15 @@ async function createSession(deps, opts) {
|
|
|
1444
1526
|
// A run may have switched branches, changed files, or spawned/finished
|
|
1445
1527
|
// background tasks. Refresh the workspace extras once it settles.
|
|
1446
1528
|
[gitBranch, gitIsRepo, gitDirtyFileCount] = await Promise.all([
|
|
1447
|
-
getGitBranch(
|
|
1448
|
-
isGitRepo(
|
|
1449
|
-
getGitDirtyFileCount(
|
|
1529
|
+
getGitBranch(gitCwd).catch(() => gitBranch),
|
|
1530
|
+
isGitRepo(gitCwd).catch(() => gitIsRepo),
|
|
1531
|
+
getGitDirtyFileCount(gitCwd).catch(() => gitDirtyFileCount),
|
|
1450
1532
|
]);
|
|
1451
1533
|
// A run may have opened/closed issues or PRs — refresh fire-and-forget so
|
|
1452
1534
|
// teardown isn't delayed by the network. Broadcasts itself on change.
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1535
|
+
repoWatch.gitHubCounts?.refresh();
|
|
1536
|
+
repoWatch.ci.refresh();
|
|
1537
|
+
repoWatch.health?.refresh();
|
|
1456
1538
|
// Serialize behind any marker/tool-triggered refresh so the terminal
|
|
1457
1539
|
// progress snapshot uses the live plan file. Once every canonical step
|
|
1458
1540
|
// is complete, remove the approved plan from future system prompts and
|
|
@@ -1518,7 +1600,7 @@ async function createSession(deps, opts) {
|
|
|
1518
1600
|
// string the webview sends on a manual Accept (see PlanReviewModal's accept
|
|
1519
1601
|
// handler in ezcoder-app/src/App.tsx). Keep the two in lockstep so auto- and
|
|
1520
1602
|
// manual approval produce identical implementation turns.
|
|
1521
|
-
const IMPLEMENT_PLAN_PROMPT =
|
|
1603
|
+
const IMPLEMENT_PLAN_PROMPT = SHARED_IMPLEMENT_PLAN_PROMPT;
|
|
1522
1604
|
// ── Stranded-queue drain ───────────────────────────────
|
|
1523
1605
|
// A prompt POSTed while an autopilot cycle is between injected runs (build
|
|
1524
1606
|
// idle, Nolan reviewing) queues — but the queue only drains INTO a running
|
|
@@ -1795,39 +1877,82 @@ async function createSession(deps, opts) {
|
|
|
1795
1877
|
// (they change outside the agent: editor saves, commits, teammates). Branch/
|
|
1796
1878
|
// repo state already refreshes after agent runs. The pollers are shared with
|
|
1797
1879
|
// every other window on the same repo; each window broadcasts only changes.
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
gitDirtyFileCount = next;
|
|
1803
|
-
broadcast("extras", footerExtras());
|
|
1804
|
-
});
|
|
1805
|
-
// Failed checks publish nothing, so the chips keep their last-known numbers
|
|
1806
|
-
// instead of flickering off on a timeout. None when origin isn't GitHub.
|
|
1807
|
-
const gitHubCountsPoll = gitHubSlug
|
|
1808
|
-
? repoPolls.gitHubCounts.subscribe(gitHubSlug, (counts) => {
|
|
1809
|
-
if (counts.issues === gitHubIssues && counts.prs === gitHubPRs)
|
|
1880
|
+
function watchRepo(dir) {
|
|
1881
|
+
const repoKey = path.resolve(dir);
|
|
1882
|
+
const dirtyFiles = repoPolls.dirtyFiles.subscribe(repoKey, (next) => {
|
|
1883
|
+
if (next === gitDirtyFileCount)
|
|
1810
1884
|
return;
|
|
1811
|
-
|
|
1812
|
-
gitHubPRs = counts.prs;
|
|
1885
|
+
gitDirtyFileCount = next;
|
|
1813
1886
|
broadcast("extras", footerExtras());
|
|
1814
|
-
})
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1887
|
+
});
|
|
1888
|
+
// Failed checks publish nothing, so the chips keep their last-known numbers
|
|
1889
|
+
// instead of flickering off on a timeout. None when origin isn't GitHub.
|
|
1890
|
+
const gitHubCounts = gitHubSlug
|
|
1891
|
+
? repoPolls.gitHubCounts.subscribe(gitHubSlug, (counts) => {
|
|
1892
|
+
if (counts.issues === gitHubIssues && counts.prs === gitHubPRs)
|
|
1893
|
+
return;
|
|
1894
|
+
gitHubIssues = counts.issues;
|
|
1895
|
+
gitHubPRs = counts.prs;
|
|
1896
|
+
broadcast("extras", footerExtras());
|
|
1897
|
+
})
|
|
1898
|
+
: null;
|
|
1899
|
+
const ci = repoPolls.gitHubCI.subscribe(repoKey, (next) => {
|
|
1900
|
+
if (JSON.stringify(next) === JSON.stringify(gitHubCI))
|
|
1826
1901
|
return;
|
|
1827
|
-
|
|
1902
|
+
gitHubCI = next;
|
|
1828
1903
|
broadcast("extras", footerExtras());
|
|
1829
|
-
})
|
|
1830
|
-
|
|
1904
|
+
});
|
|
1905
|
+
// Project Health is a code-workspace signal; chat and motion windows skip the scan.
|
|
1906
|
+
const health = mode === "code"
|
|
1907
|
+
? repoPolls.projectHealth.subscribe(repoKey, (next) => {
|
|
1908
|
+
if (JSON.stringify(next) === JSON.stringify(projectHealthScan))
|
|
1909
|
+
return;
|
|
1910
|
+
projectHealthScan = next;
|
|
1911
|
+
broadcast("extras", footerExtras());
|
|
1912
|
+
})
|
|
1913
|
+
: null;
|
|
1914
|
+
return {
|
|
1915
|
+
gitHubCounts,
|
|
1916
|
+
ci,
|
|
1917
|
+
health,
|
|
1918
|
+
unsubscribe() {
|
|
1919
|
+
dirtyFiles.unsubscribe();
|
|
1920
|
+
gitHubCounts?.unsubscribe();
|
|
1921
|
+
ci.unsubscribe();
|
|
1922
|
+
health?.unsubscribe();
|
|
1923
|
+
},
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
let repoWatch = watchRepo(gitCwd);
|
|
1927
|
+
let repoWatchStopped = false;
|
|
1928
|
+
let gitTargetGeneration = 0;
|
|
1929
|
+
/**
|
|
1930
|
+
* A goal's attached folders changed: point the titlebar's git state at the
|
|
1931
|
+
* new project folder. No-op when the target is unchanged; a newer call or
|
|
1932
|
+
* teardown supersedes one still reading git.
|
|
1933
|
+
*/
|
|
1934
|
+
async function retargetGit() {
|
|
1935
|
+
const generation = ++gitTargetGeneration;
|
|
1936
|
+
const next = await resolveGitCwd(cwd, goal?.record ?? null);
|
|
1937
|
+
if (generation !== gitTargetGeneration || repoWatchStopped || next === gitCwd)
|
|
1938
|
+
return;
|
|
1939
|
+
const state = await readGitState(next);
|
|
1940
|
+
if (generation !== gitTargetGeneration || repoWatchStopped)
|
|
1941
|
+
return;
|
|
1942
|
+
repoWatch.unsubscribe();
|
|
1943
|
+
gitCwd = next;
|
|
1944
|
+
gitBranch = state.branch;
|
|
1945
|
+
gitIsRepo = state.isRepo;
|
|
1946
|
+
gitDirtyFileCount = state.dirtyFileCount;
|
|
1947
|
+
gitHubSlug = state.gitHubSlug;
|
|
1948
|
+
// The old repo's numbers don't apply; the new watchers fill these in.
|
|
1949
|
+
gitHubIssues = null;
|
|
1950
|
+
gitHubPRs = null;
|
|
1951
|
+
gitHubCI = null;
|
|
1952
|
+
projectHealthScan = null;
|
|
1953
|
+
repoWatch = watchRepo(gitCwd);
|
|
1954
|
+
broadcast("extras", footerExtras());
|
|
1955
|
+
}
|
|
1831
1956
|
const routeCtx = {
|
|
1832
1957
|
get session() {
|
|
1833
1958
|
return session;
|
|
@@ -1879,6 +2004,23 @@ async function createSession(deps, opts) {
|
|
|
1879
2004
|
cwd,
|
|
1880
2005
|
asks,
|
|
1881
2006
|
runClaim,
|
|
2007
|
+
sessionId: opts.id,
|
|
2008
|
+
sessionIdsAt,
|
|
2009
|
+
busyAt,
|
|
2010
|
+
get goal() {
|
|
2011
|
+
return goal;
|
|
2012
|
+
},
|
|
2013
|
+
set goal(v) {
|
|
2014
|
+
const foldersBefore = JSON.stringify(goal?.record.context ?? []);
|
|
2015
|
+
goal = v;
|
|
2016
|
+
if (JSON.stringify(goal?.record.context ?? []) !== foldersBefore) {
|
|
2017
|
+
retargetGit().catch((err) => {
|
|
2018
|
+
log("WARN", "app-sidecar", "goal git retarget failed", {
|
|
2019
|
+
message: err instanceof Error ? err.message : String(err),
|
|
2020
|
+
});
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
},
|
|
1882
2024
|
broadcast,
|
|
1883
2025
|
get scheduledRunActive() {
|
|
1884
2026
|
return scheduledRunActive;
|
|
@@ -2036,6 +2178,8 @@ async function createSession(deps, opts) {
|
|
|
2036
2178
|
return;
|
|
2037
2179
|
if (handleWorkspaceRoutes(routeCtx, req, res, url, method))
|
|
2038
2180
|
return;
|
|
2181
|
+
if (handleGoalRoutes(routeCtx, req, res, url, method))
|
|
2182
|
+
return;
|
|
2039
2183
|
if (handleHistoryRoutes(routeCtx, req, res, url, method))
|
|
2040
2184
|
return;
|
|
2041
2185
|
if (handlePromptRoutes(routeCtx, req, res, url, method))
|
|
@@ -2053,6 +2197,12 @@ async function createSession(deps, opts) {
|
|
|
2053
2197
|
json(res, 404, { error: "not found" });
|
|
2054
2198
|
}
|
|
2055
2199
|
async function dispose() {
|
|
2200
|
+
// A window closed (or switched project) mid-run: stop the run itself, any
|
|
2201
|
+
// run-all sweep or autopilot turn, and a model download this window
|
|
2202
|
+
// started. Otherwise they keep streaming tokens and running tools on a
|
|
2203
|
+
// session that is being torn down.
|
|
2204
|
+
abortOwnedWork();
|
|
2205
|
+
cancelHfPull();
|
|
2056
2206
|
releaseRunAwake?.();
|
|
2057
2207
|
releaseRunAwake = null;
|
|
2058
2208
|
releaseSubagentAwake?.();
|
|
@@ -2062,10 +2212,8 @@ async function createSession(deps, opts) {
|
|
|
2062
2212
|
tasksPollStopped = true;
|
|
2063
2213
|
if (tasksPoll)
|
|
2064
2214
|
clearTimeout(tasksPoll);
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
ciPoll.unsubscribe();
|
|
2068
|
-
healthPoll?.unsubscribe();
|
|
2215
|
+
repoWatchStopped = true;
|
|
2216
|
+
repoWatch.unsubscribe();
|
|
2069
2217
|
// Stop the Telegram serve loop + dispose its per-chat sessions.
|
|
2070
2218
|
if (serveController)
|
|
2071
2219
|
await serveController.stop().catch(() => { });
|
|
@@ -2096,6 +2244,10 @@ async function createSession(deps, opts) {
|
|
|
2096
2244
|
handle,
|
|
2097
2245
|
dispose,
|
|
2098
2246
|
stopBackgroundProcesses,
|
|
2247
|
+
isBusy: () => running ||
|
|
2248
|
+
runClaim.active ||
|
|
2249
|
+
autopilotActive ||
|
|
2250
|
+
session.listSubAgents().some((w) => w.state === "running" || w.state === "starting"),
|
|
2099
2251
|
};
|
|
2100
2252
|
}
|
|
2101
2253
|
main().catch(async (err) => {
|