aoaoe 2.5.0 → 3.0.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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  <a href="https://github.com/Talador12/agent-of-agent-of-empires/actions/workflows/ci.yml"><img src="https://github.com/Talador12/agent-of-agent-of-empires/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
5
5
  <a href="https://www.npmjs.com/package/aoaoe"><img src="https://img.shields.io/npm/v/aoaoe" alt="npm version"></a>
6
6
  <a href="https://github.com/Talador12/agent-of-agent-of-empires/releases"><img src="https://img.shields.io/github/v/release/Talador12/agent-of-agent-of-empires" alt="GitHub release"></a>
7
- <img src="https://img.shields.io/badge/tests-3332-brightgreen" alt="tests">
7
+ <img src="https://img.shields.io/badge/tests-3491-brightgreen" alt="tests">
8
8
  <img src="https://img.shields.io/badge/node-%3E%3D20-blue" alt="Node.js >= 20">
9
9
  <img src="https://img.shields.io/badge/runtime%20deps-0-brightgreen" alt="zero runtime dependencies">
10
10
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
@@ -0,0 +1,30 @@
1
+ export interface CostTag {
2
+ key: string;
3
+ value: string;
4
+ }
5
+ export interface TaggedSession {
6
+ sessionTitle: string;
7
+ tags: CostTag[];
8
+ costUsd: number;
9
+ }
10
+ export interface TagReport {
11
+ tagKey: string;
12
+ groups: Array<{
13
+ value: string;
14
+ sessions: number;
15
+ totalCostUsd: number;
16
+ }>;
17
+ }
18
+ /**
19
+ * Group sessions by a tag key and sum costs.
20
+ */
21
+ export declare function groupByTag(sessions: TaggedSession[], tagKey: string): TagReport;
22
+ /**
23
+ * Format tag report for TUI display.
24
+ */
25
+ export declare function formatTagReport(report: TagReport): string[];
26
+ /**
27
+ * Parse tags from a config string: "team=platform,project=aoaoe"
28
+ */
29
+ export declare function parseTags(tagStr: string): CostTag[];
30
+ //# sourceMappingURL=cost-allocation-tags.d.ts.map
@@ -0,0 +1,47 @@
1
+ // cost-allocation-tags.ts — label sessions by team/project for cost attribution.
2
+ // tags are key-value pairs attached to sessions, used to group costs in reports.
3
+ /**
4
+ * Group sessions by a tag key and sum costs.
5
+ */
6
+ export function groupByTag(sessions, tagKey) {
7
+ const groups = new Map();
8
+ for (const s of sessions) {
9
+ const tag = s.tags.find((t) => t.key === tagKey);
10
+ const value = tag?.value ?? "(untagged)";
11
+ const group = groups.get(value) ?? { sessions: 0, totalCostUsd: 0 };
12
+ group.sessions++;
13
+ group.totalCostUsd += s.costUsd;
14
+ groups.set(value, group);
15
+ }
16
+ return {
17
+ tagKey,
18
+ groups: [...groups.entries()]
19
+ .map(([value, g]) => ({ value, ...g }))
20
+ .sort((a, b) => b.totalCostUsd - a.totalCostUsd),
21
+ };
22
+ }
23
+ /**
24
+ * Format tag report for TUI display.
25
+ */
26
+ export function formatTagReport(report) {
27
+ if (report.groups.length === 0)
28
+ return [` (no sessions tagged with "${report.tagKey}")`];
29
+ const lines = [];
30
+ lines.push(` Cost by ${report.tagKey}:`);
31
+ for (const g of report.groups) {
32
+ lines.push(` ${g.value.padEnd(20)} ${g.sessions} session${g.sessions !== 1 ? "s" : ""} $${g.totalCostUsd.toFixed(2)}`);
33
+ }
34
+ return lines;
35
+ }
36
+ /**
37
+ * Parse tags from a config string: "team=platform,project=aoaoe"
38
+ */
39
+ export function parseTags(tagStr) {
40
+ if (!tagStr)
41
+ return [];
42
+ return tagStr.split(",").map((pair) => {
43
+ const [key, ...rest] = pair.split("=");
44
+ return { key: key.trim(), value: rest.join("=").trim() };
45
+ }).filter((t) => t.key && t.value);
46
+ }
47
+ //# sourceMappingURL=cost-allocation-tags.js.map
@@ -0,0 +1,20 @@
1
+ import type { TaskState } from "./types.js";
2
+ export interface SimilarityPair {
3
+ titleA: string;
4
+ titleB: string;
5
+ similarity: number;
6
+ sharedKeywords: string[];
7
+ }
8
+ /**
9
+ * Compute Jaccard similarity between two keyword sets.
10
+ */
11
+ export declare function jaccardSimilarity(setA: Set<string>, setB: Set<string>): number;
12
+ /**
13
+ * Find pairs of tasks with similar goals.
14
+ */
15
+ export declare function findSimilarGoals(tasks: readonly TaskState[], threshold?: number): SimilarityPair[];
16
+ /**
17
+ * Format similarity pairs for TUI display.
18
+ */
19
+ export declare function formatSimilarGoals(pairs: SimilarityPair[]): string[];
20
+ //# sourceMappingURL=goal-similarity.d.ts.map
@@ -0,0 +1,55 @@
1
+ // goal-similarity.ts — detect sessions with overlapping goals for coordination.
2
+ // uses Jaccard similarity on keyword sets to find related tasks.
3
+ import { extractKeywords } from "./drift-detector.js";
4
+ /**
5
+ * Compute Jaccard similarity between two keyword sets.
6
+ */
7
+ export function jaccardSimilarity(setA, setB) {
8
+ if (setA.size === 0 && setB.size === 0)
9
+ return 0;
10
+ let intersection = 0;
11
+ for (const w of setA)
12
+ if (setB.has(w))
13
+ intersection++;
14
+ const union = setA.size + setB.size - intersection;
15
+ return union > 0 ? intersection / union : 0;
16
+ }
17
+ /**
18
+ * Find pairs of tasks with similar goals.
19
+ */
20
+ export function findSimilarGoals(tasks, threshold = 0.3) {
21
+ const keywordSets = tasks.map((t) => ({
22
+ title: t.sessionTitle,
23
+ keywords: new Set(extractKeywords(t.goal)),
24
+ }));
25
+ const pairs = [];
26
+ for (let i = 0; i < keywordSets.length; i++) {
27
+ for (let j = i + 1; j < keywordSets.length; j++) {
28
+ const sim = jaccardSimilarity(keywordSets[i].keywords, keywordSets[j].keywords);
29
+ if (sim >= threshold) {
30
+ const shared = [...keywordSets[i].keywords].filter((w) => keywordSets[j].keywords.has(w));
31
+ pairs.push({
32
+ titleA: keywordSets[i].title,
33
+ titleB: keywordSets[j].title,
34
+ similarity: Math.round(sim * 100) / 100,
35
+ sharedKeywords: shared,
36
+ });
37
+ }
38
+ }
39
+ }
40
+ return pairs.sort((a, b) => b.similarity - a.similarity);
41
+ }
42
+ /**
43
+ * Format similarity pairs for TUI display.
44
+ */
45
+ export function formatSimilarGoals(pairs) {
46
+ if (pairs.length === 0)
47
+ return [" (no similar goals detected)"];
48
+ const lines = [];
49
+ lines.push(` Similar goals (${pairs.length} pair${pairs.length !== 1 ? "s" : ""}):`);
50
+ for (const p of pairs) {
51
+ lines.push(` ${Math.round(p.similarity * 100)}% — "${p.titleA}" ↔ "${p.titleB}" [${p.sharedKeywords.join(", ")}]`);
52
+ }
53
+ return lines;
54
+ }
55
+ //# sourceMappingURL=goal-similarity.js.map
package/dist/index.js CHANGED
@@ -76,6 +76,13 @@ import { defaultAlertRules, evaluateAlertRules, formatAlertRules } from "./alert
76
76
  import { forecastHealth, formatHealthForecast } from "./health-forecast.js";
77
77
  import { tailSession, formatTail, parseTailArgs } from "./session-tail.js";
78
78
  import { renderWorkflowDag, renderChainDag } from "./workflow-viz.js";
79
+ import { formatPrometheusMetrics, buildMetricsSnapshot } from "./metrics-export.js";
80
+ import { grepArchives, formatGrepResult } from "./fleet-grep.js";
81
+ import { createExecution, advanceExecution, formatExecution } from "./runbook-executor.js";
82
+ import { cloneSession, formatCloneResult } from "./session-clone.js";
83
+ import { findSimilarGoals, formatSimilarGoals } from "./goal-similarity.js";
84
+ import { groupByTag, formatTagReport, parseTags } from "./cost-allocation-tags.js";
85
+ import { recommendScaling, formatScalingRecommendation } from "./predictive-scaling.js";
79
86
  import { buildLifecycleRecords, computeLifecycleStats, formatLifecycleStats } from "./lifecycle-analytics.js";
80
87
  import { buildCostAttributions, computeCostReport, formatCostReport } from "./cost-attribution.js";
81
88
  import { decomposeGoal, formatDecomposition } from "./goal-decomposer.js";
@@ -585,6 +592,7 @@ async function main() {
585
592
  const tokenQuotaManager = new TokenQuotaManager();
586
593
  const abReasoningTracker = new ABReasoningTracker(config.reasoner, "claude-code");
587
594
  const alertRules = defaultAlertRules();
595
+ let activeRunbookExec = null;
588
596
  // checkpoint restore: load previous daemon state if available
589
597
  if (shouldRestoreCheckpoint()) {
590
598
  const cp = loadCheckpoint();
@@ -2666,6 +2674,133 @@ async function main() {
2666
2674
  tui.log("system", "workflow-viz: no active workflow or chain");
2667
2675
  }
2668
2676
  });
2677
+ // wire /metrics — Prometheus metrics snapshot
2678
+ input.onMetrics(() => {
2679
+ const sessions = tui.getSessions();
2680
+ const tasks = taskManager?.tasks ?? [];
2681
+ const scores = sessions.map((s) => s.status === "working" || s.status === "running" ? 80 : s.status === "error" ? 20 : 50);
2682
+ const health = scores.length > 0 ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) : 100;
2683
+ let cost = 0;
2684
+ for (const s of sessions) {
2685
+ const m = s.costStr?.match(/\$(\d+(?:\.\d+)?)/);
2686
+ if (m)
2687
+ cost += parseFloat(m[1]);
2688
+ }
2689
+ const cacheStats = observationCache.getStats();
2690
+ const reasonerStats = reasonerCostTracker.getSummary();
2691
+ const nudgeReport = nudgeTracker.getReport();
2692
+ const snapshot = buildMetricsSnapshot({
2693
+ fleetHealth: health, totalSessions: sessions.length,
2694
+ activeSessions: sessions.filter((s) => s.status === "working" || s.status === "running").length,
2695
+ errorSessions: sessions.filter((s) => s.status === "error").length,
2696
+ totalTasks: tasks.length, activeTasks: tasks.filter((t) => t.status === "active").length,
2697
+ completedTasks: tasks.filter((t) => t.status === "completed").length,
2698
+ failedTasks: tasks.filter((t) => t.status === "failed").length,
2699
+ totalCostUsd: cost, reasonerCallsTotal: reasonerStats.totalCalls,
2700
+ reasonerCostTotal: reasonerStats.totalCostUsd,
2701
+ cacheHits: cacheStats.totalHits, cacheMisses: cacheStats.totalMisses,
2702
+ nudgesSent: nudgeReport.totalNudges, nudgesEffective: nudgeReport.effectiveNudges,
2703
+ pollIntervalMs: adaptivePollController.intervalMs,
2704
+ uptimeMs: Date.now() - daemonStartedAt,
2705
+ });
2706
+ const text = formatPrometheusMetrics(snapshot);
2707
+ for (const l of text.split("\n").filter(Boolean))
2708
+ tui.log("system", l);
2709
+ });
2710
+ // wire /fleet-grep — search archived outputs
2711
+ input.onFleetGrep((pattern) => {
2712
+ const result = grepArchives(pattern);
2713
+ const lines = formatGrepResult(result);
2714
+ for (const l of lines)
2715
+ tui.log("system", l);
2716
+ });
2717
+ // wire /runbook-exec — execute/advance runbook
2718
+ input.onRunbookExec(() => {
2719
+ if (!activeRunbookExec) {
2720
+ const runbooks = generateRunbooks();
2721
+ if (runbooks.length === 0) {
2722
+ tui.log("system", "runbook-exec: no runbooks to execute (need audit data)");
2723
+ return;
2724
+ }
2725
+ activeRunbookExec = createExecution(runbooks[0]);
2726
+ tui.log("+ action", `runbook-exec: starting "${runbooks[0].title}"`);
2727
+ }
2728
+ const step = advanceExecution(activeRunbookExec);
2729
+ if (step) {
2730
+ tui.log("system", `runbook-exec: executing step — ${step.action}: ${step.detail}`);
2731
+ }
2732
+ else {
2733
+ const lines = formatExecution(activeRunbookExec);
2734
+ for (const l of lines)
2735
+ tui.log("system", l);
2736
+ activeRunbookExec = null;
2737
+ }
2738
+ });
2739
+ // wire /clone — clone a session
2740
+ input.onClone((args) => {
2741
+ const parts = args.split(/\s+/);
2742
+ const [sourceTitle, cloneTitle, ...goalParts] = parts;
2743
+ if (!sourceTitle || !cloneTitle) {
2744
+ tui.log("system", "clone: usage: /clone <source> <new-name> [goal]");
2745
+ return;
2746
+ }
2747
+ const tasks = taskManager?.tasks ?? [];
2748
+ const source = tasks.find((t) => t.sessionTitle.toLowerCase() === sourceTitle.toLowerCase());
2749
+ if (!source) {
2750
+ tui.log("system", `clone: source "${sourceTitle}" not found`);
2751
+ return;
2752
+ }
2753
+ const goalOverride = goalParts.length > 0 ? goalParts.join(" ") : undefined;
2754
+ const def = cloneSession(source, { sourceTitle, cloneTitle, goalOverride });
2755
+ tui.log("+ action", `cloned "${sourceTitle}" → "${cloneTitle}"`);
2756
+ const lines = formatCloneResult({ original: sourceTitle, clone: cloneTitle, goal: def.goal, tool: def.tool ?? "opencode" });
2757
+ for (const l of lines)
2758
+ tui.log("system", l);
2759
+ });
2760
+ // wire /similar-goals — find overlapping goals
2761
+ input.onSimilarGoals(() => {
2762
+ const tasks = taskManager?.tasks ?? [];
2763
+ const pairs = findSimilarGoals(tasks);
2764
+ const lines = formatSimilarGoals(pairs);
2765
+ for (const l of lines)
2766
+ tui.log("system", l);
2767
+ });
2768
+ // wire /cost-tags — group costs by tag
2769
+ input.onCostTags((tagKey) => {
2770
+ const tasks = taskManager?.tasks ?? [];
2771
+ const sessions = tui.getSessions();
2772
+ const tagged = tasks.map((t) => {
2773
+ const s = sessions.find((s) => s.title === t.sessionTitle);
2774
+ let cost = 0;
2775
+ if (s?.costStr) {
2776
+ const m = s.costStr.match(/\$(\d+(?:\.\d+)?)/);
2777
+ if (m)
2778
+ cost = parseFloat(m[1]);
2779
+ }
2780
+ return { sessionTitle: t.sessionTitle, tags: parseTags(t.tags ?? ""), costUsd: cost };
2781
+ });
2782
+ const report = groupByTag(tagged, tagKey);
2783
+ const lines = formatTagReport(report);
2784
+ for (const l of lines)
2785
+ tui.log("system", l);
2786
+ });
2787
+ // wire /scaling — predictive pool scaling
2788
+ input.onScaling(() => {
2789
+ const tasks = taskManager?.tasks ?? [];
2790
+ const poolStatus = sessionPoolManager.getStatus(tasks);
2791
+ const activeSessions = poolStatus.activeCount;
2792
+ const pendingTasks = poolStatus.pendingCount;
2793
+ const utilPct = poolStatus.maxConcurrent > 0 ? Math.round((activeSessions / poolStatus.maxConcurrent) * 100) : 0;
2794
+ const rec = recommendScaling({
2795
+ currentPoolSize: poolStatus.maxConcurrent,
2796
+ activeSessions, pendingTasks,
2797
+ recentUtilizationPct: utilPct, peakUtilizationPct: utilPct,
2798
+ averageTaskDurationMs: 3_600_000,
2799
+ });
2800
+ const lines = formatScalingRecommendation(rec);
2801
+ for (const l of lines)
2802
+ tui.log("system", l);
2803
+ });
2669
2804
  input.onCostSummary(() => {
2670
2805
  const sessions = tui.getSessions();
2671
2806
  const summary = computeCostSummary(sessions, tui.getAllSessionCosts());
package/dist/input.d.ts CHANGED
@@ -148,6 +148,13 @@ export type AlertRulesHandler = () => void;
148
148
  export type SessionTailHandler = (args: string) => void;
149
149
  export type HealthForecastHandler = () => void;
150
150
  export type WorkflowVizHandler = () => void;
151
+ export type MetricsHandler = () => void;
152
+ export type FleetGrepHandler = (pattern: string) => void;
153
+ export type RunbookExecHandler = () => void;
154
+ export type CloneHandler = (args: string) => void;
155
+ export type SimilarGoalsHandler = () => void;
156
+ export type CostTagsHandler = (tagKey: string) => void;
157
+ export type ScalingHandler = () => void;
151
158
  export interface MouseEvent {
152
159
  button: number;
153
160
  col: number;
@@ -494,6 +501,20 @@ export declare class InputReader {
494
501
  onSessionTail(handler: SessionTailHandler): void;
495
502
  onHealthForecast(handler: HealthForecastHandler): void;
496
503
  onWorkflowViz(handler: WorkflowVizHandler): void;
504
+ private metricsHandler;
505
+ private fleetGrepHandler;
506
+ private runbookExecHandler;
507
+ private cloneHandler;
508
+ private similarGoalsHandler;
509
+ private costTagsHandler;
510
+ private scalingHandler;
511
+ onMetrics(handler: MetricsHandler): void;
512
+ onFleetGrep(handler: FleetGrepHandler): void;
513
+ onRunbookExec(handler: RunbookExecHandler): void;
514
+ onClone(handler: CloneHandler): void;
515
+ onSimilarGoals(handler: SimilarGoalsHandler): void;
516
+ onCostTags(handler: CostTagsHandler): void;
517
+ onScaling(handler: ScalingHandler): void;
497
518
  onFleetSearch(handler: FleetSearchHandler): void;
498
519
  onNudgeStats(handler: NudgeStatsHandler): void;
499
520
  onAllocation(handler: AllocationHandler): void;
package/dist/input.js CHANGED
@@ -574,6 +574,20 @@ export class InputReader {
574
574
  onSessionTail(handler) { this.sessionTailHandler = handler; }
575
575
  onHealthForecast(handler) { this.healthForecastHandler = handler; }
576
576
  onWorkflowViz(handler) { this.workflowVizHandler = handler; }
577
+ metricsHandler = null;
578
+ fleetGrepHandler = null;
579
+ runbookExecHandler = null;
580
+ cloneHandler = null;
581
+ similarGoalsHandler = null;
582
+ costTagsHandler = null;
583
+ scalingHandler = null;
584
+ onMetrics(handler) { this.metricsHandler = handler; }
585
+ onFleetGrep(handler) { this.fleetGrepHandler = handler; }
586
+ onRunbookExec(handler) { this.runbookExecHandler = handler; }
587
+ onClone(handler) { this.cloneHandler = handler; }
588
+ onSimilarGoals(handler) { this.similarGoalsHandler = handler; }
589
+ onCostTags(handler) { this.costTagsHandler = handler; }
590
+ onScaling(handler) { this.scalingHandler = handler; }
577
591
  onFleetSearch(handler) { this.fleetSearchHandler = handler; }
578
592
  onNudgeStats(handler) { this.nudgeStatsHandler = handler; }
579
593
  onAllocation(handler) { this.allocationHandler = handler; }
@@ -2524,6 +2538,62 @@ ${BOLD}other:${RESET}
2524
2538
  else
2525
2539
  console.error(`${DIM}workflow-viz not available (no TUI)${RESET}`);
2526
2540
  break;
2541
+ case "/metrics":
2542
+ if (this.metricsHandler)
2543
+ this.metricsHandler();
2544
+ else
2545
+ console.error(`${DIM}metrics not available (no TUI)${RESET}`);
2546
+ break;
2547
+ case "/fleet-grep": {
2548
+ const fgArg = line.slice("/fleet-grep".length).trim();
2549
+ if (!fgArg) {
2550
+ console.error(`${DIM}usage: /fleet-grep <pattern>${RESET}`);
2551
+ break;
2552
+ }
2553
+ if (this.fleetGrepHandler)
2554
+ this.fleetGrepHandler(fgArg);
2555
+ else
2556
+ console.error(`${DIM}fleet-grep not available (no TUI)${RESET}`);
2557
+ break;
2558
+ }
2559
+ case "/runbook-exec":
2560
+ if (this.runbookExecHandler)
2561
+ this.runbookExecHandler();
2562
+ else
2563
+ console.error(`${DIM}runbook-exec not available (no TUI)${RESET}`);
2564
+ break;
2565
+ case "/clone": {
2566
+ const clArg = line.slice("/clone".length).trim();
2567
+ if (!clArg) {
2568
+ console.error(`${DIM}usage: /clone <source> <new-name> [goal-override]${RESET}`);
2569
+ break;
2570
+ }
2571
+ if (this.cloneHandler)
2572
+ this.cloneHandler(clArg);
2573
+ else
2574
+ console.error(`${DIM}clone not available (no TUI)${RESET}`);
2575
+ break;
2576
+ }
2577
+ case "/similar-goals":
2578
+ if (this.similarGoalsHandler)
2579
+ this.similarGoalsHandler();
2580
+ else
2581
+ console.error(`${DIM}similar-goals not available (no TUI)${RESET}`);
2582
+ break;
2583
+ case "/cost-tags": {
2584
+ const ctArg = line.slice("/cost-tags".length).trim() || "team";
2585
+ if (this.costTagsHandler)
2586
+ this.costTagsHandler(ctArg);
2587
+ else
2588
+ console.error(`${DIM}cost-tags not available (no TUI)${RESET}`);
2589
+ break;
2590
+ }
2591
+ case "/scaling":
2592
+ if (this.scalingHandler)
2593
+ this.scalingHandler();
2594
+ else
2595
+ console.error(`${DIM}scaling not available (no TUI)${RESET}`);
2596
+ break;
2527
2597
  case "/clear":
2528
2598
  process.stderr.write("\x1b[2J\x1b[H");
2529
2599
  break;
@@ -0,0 +1,24 @@
1
+ export interface ScalingRecommendation {
2
+ currentPoolSize: number;
3
+ recommendedPoolSize: number;
4
+ action: "scale-up" | "scale-down" | "maintain";
5
+ reason: string;
6
+ confidence: "low" | "medium" | "high";
7
+ }
8
+ export interface ScalingInput {
9
+ currentPoolSize: number;
10
+ activeSessions: number;
11
+ pendingTasks: number;
12
+ recentUtilizationPct: number;
13
+ peakUtilizationPct: number;
14
+ averageTaskDurationMs: number;
15
+ }
16
+ /**
17
+ * Compute a scaling recommendation from current fleet state.
18
+ */
19
+ export declare function recommendScaling(input: ScalingInput): ScalingRecommendation;
20
+ /**
21
+ * Format scaling recommendation for TUI display.
22
+ */
23
+ export declare function formatScalingRecommendation(rec: ScalingRecommendation): string[];
24
+ //# sourceMappingURL=predictive-scaling.d.ts.map
@@ -0,0 +1,60 @@
1
+ // predictive-scaling.ts — auto-adjust pool size based on workload patterns.
2
+ // analyzes historical utilization and upcoming task queue to recommend
3
+ // scaling the concurrent session pool up or down.
4
+ /**
5
+ * Compute a scaling recommendation from current fleet state.
6
+ */
7
+ export function recommendScaling(input) {
8
+ const { currentPoolSize, activeSessions, pendingTasks, recentUtilizationPct, peakUtilizationPct } = input;
9
+ // scale up if utilization is consistently high and tasks are queued
10
+ if (recentUtilizationPct > 80 && pendingTasks > 0) {
11
+ const newSize = Math.min(currentPoolSize * 2, currentPoolSize + pendingTasks);
12
+ return {
13
+ currentPoolSize,
14
+ recommendedPoolSize: Math.ceil(newSize),
15
+ action: "scale-up",
16
+ reason: `${recentUtilizationPct}% utilization with ${pendingTasks} pending tasks`,
17
+ confidence: pendingTasks > 3 ? "high" : "medium",
18
+ };
19
+ }
20
+ // scale up if peak utilization hit 100% (pool was fully saturated)
21
+ if (peakUtilizationPct >= 100 && pendingTasks > 0) {
22
+ return {
23
+ currentPoolSize,
24
+ recommendedPoolSize: currentPoolSize + Math.ceil(pendingTasks * 0.5),
25
+ action: "scale-up",
26
+ reason: `pool saturated (peak ${peakUtilizationPct}%) with ${pendingTasks} waiting`,
27
+ confidence: "high",
28
+ };
29
+ }
30
+ // scale down if utilization is consistently low
31
+ if (recentUtilizationPct < 30 && activeSessions < currentPoolSize * 0.5 && currentPoolSize > 2) {
32
+ const newSize = Math.max(2, Math.ceil(activeSessions * 1.5));
33
+ return {
34
+ currentPoolSize,
35
+ recommendedPoolSize: newSize,
36
+ action: "scale-down",
37
+ reason: `${recentUtilizationPct}% utilization, only ${activeSessions}/${currentPoolSize} active`,
38
+ confidence: recentUtilizationPct < 15 ? "high" : "medium",
39
+ };
40
+ }
41
+ return {
42
+ currentPoolSize,
43
+ recommendedPoolSize: currentPoolSize,
44
+ action: "maintain",
45
+ reason: `utilization ${recentUtilizationPct}% is within normal range`,
46
+ confidence: "high",
47
+ };
48
+ }
49
+ /**
50
+ * Format scaling recommendation for TUI display.
51
+ */
52
+ export function formatScalingRecommendation(rec) {
53
+ const icon = rec.action === "scale-up" ? "⬆" : rec.action === "scale-down" ? "⬇" : "─";
54
+ const conf = rec.confidence === "high" ? "●" : rec.confidence === "medium" ? "◐" : "○";
55
+ return [
56
+ ` ${icon} ${conf} Pool scaling: ${rec.action} (${rec.currentPoolSize} → ${rec.recommendedPoolSize})`,
57
+ ` ${rec.reason}`,
58
+ ];
59
+ }
60
+ //# sourceMappingURL=predictive-scaling.js.map
@@ -0,0 +1,23 @@
1
+ import type { TaskState, TaskDefinition } from "./types.js";
2
+ export interface CloneOptions {
3
+ sourceTitle: string;
4
+ cloneTitle: string;
5
+ goalOverride?: string;
6
+ toolOverride?: string;
7
+ modeOverride?: string;
8
+ }
9
+ export interface CloneResult {
10
+ original: string;
11
+ clone: string;
12
+ goal: string;
13
+ tool: string;
14
+ }
15
+ /**
16
+ * Create a clone task definition from an existing task.
17
+ */
18
+ export declare function cloneSession(source: TaskState, options: CloneOptions): TaskDefinition;
19
+ /**
20
+ * Format clone result for TUI display.
21
+ */
22
+ export declare function formatCloneResult(result: CloneResult): string[];
23
+ //# sourceMappingURL=session-clone.d.ts.map
@@ -0,0 +1,26 @@
1
+ // session-clone.ts — clone a session to try alternative approaches in parallel.
2
+ // creates a new task definition from an existing session's config + goal,
3
+ // allowing A/B experimentation on the same problem.
4
+ /**
5
+ * Create a clone task definition from an existing task.
6
+ */
7
+ export function cloneSession(source, options) {
8
+ return {
9
+ repo: source.repo,
10
+ sessionTitle: options.cloneTitle,
11
+ sessionMode: (options.modeOverride ?? source.sessionMode),
12
+ tool: options.toolOverride ?? source.tool,
13
+ goal: options.goalOverride ?? source.goal,
14
+ dependsOn: undefined, // clone runs independently
15
+ };
16
+ }
17
+ /**
18
+ * Format clone result for TUI display.
19
+ */
20
+ export function formatCloneResult(result) {
21
+ return [
22
+ ` Cloned "${result.original}" → "${result.clone}"`,
23
+ ` Tool: ${result.tool} Goal: ${result.goal.length > 60 ? result.goal.slice(0, 57) + "..." : result.goal}`,
24
+ ];
25
+ }
26
+ //# sourceMappingURL=session-clone.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "2.5.0",
3
+ "version": "3.0.0",
4
4
  "description": "Autonomous supervisor for agent-of-empires sessions using OpenCode or Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",