aoaoe 2.0.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 +1 -1
- package/dist/alert-composer.d.ts +32 -0
- package/dist/alert-composer.js +51 -0
- package/dist/alert-rule-dsl.d.ts +32 -0
- package/dist/alert-rule-dsl.js +87 -0
- package/dist/cost-allocation-tags.d.ts +30 -0
- package/dist/cost-allocation-tags.js +47 -0
- package/dist/fleet-grep.d.ts +22 -0
- package/dist/fleet-grep.js +70 -0
- package/dist/goal-similarity.d.ts +20 -0
- package/dist/goal-similarity.js +55 -0
- package/dist/health-forecast.d.ts +23 -0
- package/dist/health-forecast.js +101 -0
- package/dist/index.js +191 -0
- package/dist/input.d.ts +30 -0
- package/dist/input.js +100 -0
- package/dist/metrics-export.d.ts +29 -0
- package/dist/metrics-export.js +58 -0
- package/dist/predictive-scaling.d.ts +24 -0
- package/dist/predictive-scaling.js +60 -0
- package/dist/runbook-executor.d.ts +37 -0
- package/dist/runbook-executor.js +78 -0
- package/dist/session-clone.d.ts +23 -0
- package/dist/session-clone.js +26 -0
- package/dist/session-tail.d.ts +24 -0
- package/dist/session-tail.js +52 -0
- package/dist/workflow-viz.d.ts +15 -0
- package/dist/workflow-viz.js +74 -0
- package/package.json +1 -1
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-
|
|
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,32 @@
|
|
|
1
|
+
import type { AlertContext } from "./alert-rules.js";
|
|
2
|
+
export type ComposedCondition = (ctx: AlertContext) => boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Compose multiple condition strings with AND logic.
|
|
5
|
+
* All conditions must be true for the composed condition to fire.
|
|
6
|
+
*/
|
|
7
|
+
export declare function composeAnd(expressions: string[]): ComposedCondition | null;
|
|
8
|
+
/**
|
|
9
|
+
* Compose multiple condition strings with OR logic.
|
|
10
|
+
* Any condition being true fires the composed condition.
|
|
11
|
+
*/
|
|
12
|
+
export declare function composeOr(expressions: string[]): ComposedCondition | null;
|
|
13
|
+
/**
|
|
14
|
+
* Parse a composed condition from config.
|
|
15
|
+
* Format: { "and": ["fleetHealth < 40", "errorSessions > 2"] }
|
|
16
|
+
* or { "or": ["fleetHealth < 20", "stuckSessions >= 3"] }
|
|
17
|
+
* or plain string "fleetHealth < 50"
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseComposedCondition(spec: string | {
|
|
20
|
+
and: string[];
|
|
21
|
+
} | {
|
|
22
|
+
or: string[];
|
|
23
|
+
}): ComposedCondition | null;
|
|
24
|
+
/**
|
|
25
|
+
* Format a composed condition for display.
|
|
26
|
+
*/
|
|
27
|
+
export declare function formatComposedCondition(spec: string | {
|
|
28
|
+
and: string[];
|
|
29
|
+
} | {
|
|
30
|
+
or: string[];
|
|
31
|
+
}): string;
|
|
32
|
+
//# sourceMappingURL=alert-composer.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// alert-composer.ts — AND/OR composition of alert conditions.
|
|
2
|
+
// extends alert-rule-dsl.ts with logical operators for complex rules.
|
|
3
|
+
import { parseCondition } from "./alert-rule-dsl.js";
|
|
4
|
+
/**
|
|
5
|
+
* Compose multiple condition strings with AND logic.
|
|
6
|
+
* All conditions must be true for the composed condition to fire.
|
|
7
|
+
*/
|
|
8
|
+
export function composeAnd(expressions) {
|
|
9
|
+
const fns = expressions.map(parseCondition);
|
|
10
|
+
if (fns.some((f) => f === null))
|
|
11
|
+
return null;
|
|
12
|
+
return (ctx) => fns.every((f) => f(ctx));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Compose multiple condition strings with OR logic.
|
|
16
|
+
* Any condition being true fires the composed condition.
|
|
17
|
+
*/
|
|
18
|
+
export function composeOr(expressions) {
|
|
19
|
+
const fns = expressions.map(parseCondition);
|
|
20
|
+
if (fns.some((f) => f === null))
|
|
21
|
+
return null;
|
|
22
|
+
return (ctx) => fns.some((f) => f(ctx));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Parse a composed condition from config.
|
|
26
|
+
* Format: { "and": ["fleetHealth < 40", "errorSessions > 2"] }
|
|
27
|
+
* or { "or": ["fleetHealth < 20", "stuckSessions >= 3"] }
|
|
28
|
+
* or plain string "fleetHealth < 50"
|
|
29
|
+
*/
|
|
30
|
+
export function parseComposedCondition(spec) {
|
|
31
|
+
if (typeof spec === "string")
|
|
32
|
+
return parseCondition(spec);
|
|
33
|
+
if ("and" in spec)
|
|
34
|
+
return composeAnd(spec.and);
|
|
35
|
+
if ("or" in spec)
|
|
36
|
+
return composeOr(spec.or);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Format a composed condition for display.
|
|
41
|
+
*/
|
|
42
|
+
export function formatComposedCondition(spec) {
|
|
43
|
+
if (typeof spec === "string")
|
|
44
|
+
return spec;
|
|
45
|
+
if ("and" in spec)
|
|
46
|
+
return spec.and.join(" AND ");
|
|
47
|
+
if ("or" in spec)
|
|
48
|
+
return spec.or.join(" OR ");
|
|
49
|
+
return "unknown";
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=alert-composer.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AlertContext, AlertRule, AlertSeverity } from "./alert-rules.js";
|
|
2
|
+
export interface AlertRuleConfig {
|
|
3
|
+
name: string;
|
|
4
|
+
severity: AlertSeverity;
|
|
5
|
+
condition: string;
|
|
6
|
+
cooldownMin: number;
|
|
7
|
+
description?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Parse a DSL condition string into a function.
|
|
11
|
+
* Format: "<field> <op> <value>" — e.g., "fleetHealth < 40"
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseCondition(expr: string): ((ctx: AlertContext) => boolean) | null;
|
|
14
|
+
/**
|
|
15
|
+
* Convert user-defined rule configs into AlertRule objects.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseAlertRuleConfigs(configs: AlertRuleConfig[]): {
|
|
18
|
+
rules: AlertRule[];
|
|
19
|
+
errors: string[];
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Validate a DSL condition string without executing it.
|
|
23
|
+
*/
|
|
24
|
+
export declare function validateCondition(expr: string): {
|
|
25
|
+
valid: boolean;
|
|
26
|
+
error?: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Format available DSL fields for help display.
|
|
30
|
+
*/
|
|
31
|
+
export declare function formatDslHelp(): string[];
|
|
32
|
+
//# sourceMappingURL=alert-rule-dsl.d.ts.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// alert-rule-dsl.ts — user-defined alert rules via config file.
|
|
2
|
+
// parses a simple DSL for defining custom alert conditions:
|
|
3
|
+
// { "name": "my-rule", "severity": "warning", "condition": "fleetHealth < 40", "cooldownMin": 10 }
|
|
4
|
+
// supported operators and fields
|
|
5
|
+
const VALID_FIELDS = new Set(["fleetHealth", "activeSessions", "errorSessions", "totalCostUsd", "hourlyCostRate", "stuckSessions"]);
|
|
6
|
+
const VALID_OPS = new Set(["<", ">", "<=", ">=", "==", "!="]);
|
|
7
|
+
/**
|
|
8
|
+
* Parse a DSL condition string into a function.
|
|
9
|
+
* Format: "<field> <op> <value>" — e.g., "fleetHealth < 40"
|
|
10
|
+
*/
|
|
11
|
+
export function parseCondition(expr) {
|
|
12
|
+
const match = expr.trim().match(/^(\w+)\s*(<=?|>=?|[!=]=)\s*(\d+(?:\.\d+)?)$/);
|
|
13
|
+
if (!match)
|
|
14
|
+
return null;
|
|
15
|
+
const [, field, op, valueStr] = match;
|
|
16
|
+
if (!VALID_FIELDS.has(field))
|
|
17
|
+
return null;
|
|
18
|
+
if (!VALID_OPS.has(op))
|
|
19
|
+
return null;
|
|
20
|
+
const value = parseFloat(valueStr);
|
|
21
|
+
if (!isFinite(value))
|
|
22
|
+
return null;
|
|
23
|
+
return (ctx) => {
|
|
24
|
+
const actual = ctx[field];
|
|
25
|
+
if (typeof actual !== "number")
|
|
26
|
+
return false;
|
|
27
|
+
switch (op) {
|
|
28
|
+
case "<": return actual < value;
|
|
29
|
+
case ">": return actual > value;
|
|
30
|
+
case "<=": return actual <= value;
|
|
31
|
+
case ">=": return actual >= value;
|
|
32
|
+
case "==": return actual === value;
|
|
33
|
+
case "!=": return actual !== value;
|
|
34
|
+
default: return false;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Convert user-defined rule configs into AlertRule objects.
|
|
40
|
+
*/
|
|
41
|
+
export function parseAlertRuleConfigs(configs) {
|
|
42
|
+
const rules = [];
|
|
43
|
+
const errors = [];
|
|
44
|
+
for (const cfg of configs) {
|
|
45
|
+
const condition = parseCondition(cfg.condition);
|
|
46
|
+
if (!condition) {
|
|
47
|
+
errors.push(`invalid condition in rule "${cfg.name}": "${cfg.condition}"`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
rules.push({
|
|
51
|
+
name: cfg.name,
|
|
52
|
+
description: cfg.description ?? cfg.condition,
|
|
53
|
+
severity: cfg.severity,
|
|
54
|
+
condition,
|
|
55
|
+
cooldownMs: cfg.cooldownMin * 60_000,
|
|
56
|
+
lastFiredAt: 0,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return { rules, errors };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Validate a DSL condition string without executing it.
|
|
63
|
+
*/
|
|
64
|
+
export function validateCondition(expr) {
|
|
65
|
+
const match = expr.trim().match(/^(\w+)\s*(<=?|>=?|[!=]=)\s*(\d+(?:\.\d+)?)$/);
|
|
66
|
+
if (!match)
|
|
67
|
+
return { valid: false, error: `invalid format: expected "<field> <op> <value>", got "${expr}"` };
|
|
68
|
+
const [, field, op] = match;
|
|
69
|
+
if (!VALID_FIELDS.has(field))
|
|
70
|
+
return { valid: false, error: `unknown field "${field}". valid: ${[...VALID_FIELDS].join(", ")}` };
|
|
71
|
+
if (!VALID_OPS.has(op))
|
|
72
|
+
return { valid: false, error: `unknown operator "${op}". valid: ${[...VALID_OPS].join(", ")}` };
|
|
73
|
+
return { valid: true };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Format available DSL fields for help display.
|
|
77
|
+
*/
|
|
78
|
+
export function formatDslHelp() {
|
|
79
|
+
return [
|
|
80
|
+
" Alert rule DSL:",
|
|
81
|
+
" Format: <field> <op> <value>",
|
|
82
|
+
` Fields: ${[...VALID_FIELDS].join(", ")}`,
|
|
83
|
+
` Operators: ${[...VALID_OPS].join(", ")}`,
|
|
84
|
+
' Example: { "name": "low-health", "severity": "warning", "condition": "fleetHealth < 50", "cooldownMin": 10 }',
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=alert-rule-dsl.js.map
|
|
@@ -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,22 @@
|
|
|
1
|
+
export interface GrepHit {
|
|
2
|
+
archive: string;
|
|
3
|
+
lineNumber: number;
|
|
4
|
+
line: string;
|
|
5
|
+
matchStart: number;
|
|
6
|
+
matchEnd: number;
|
|
7
|
+
}
|
|
8
|
+
export interface GrepResult {
|
|
9
|
+
pattern: string;
|
|
10
|
+
totalHits: number;
|
|
11
|
+
filesSearched: number;
|
|
12
|
+
hits: GrepHit[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Search across all archived outputs for a regex pattern.
|
|
16
|
+
*/
|
|
17
|
+
export declare function grepArchives(pattern: string, maxResults?: number, maxFiles?: number): GrepResult;
|
|
18
|
+
/**
|
|
19
|
+
* Format grep results for TUI display.
|
|
20
|
+
*/
|
|
21
|
+
export declare function formatGrepResult(result: GrepResult): string[];
|
|
22
|
+
//# sourceMappingURL=fleet-grep.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// fleet-grep.ts — regex search across archived session outputs.
|
|
2
|
+
// searches gzipped archive files for matches, returning results with context.
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { gunzipSync } from "node:zlib";
|
|
7
|
+
const ARCHIVE_DIR = join(homedir(), ".aoaoe", "output-archive");
|
|
8
|
+
/**
|
|
9
|
+
* Search across all archived outputs for a regex pattern.
|
|
10
|
+
*/
|
|
11
|
+
export function grepArchives(pattern, maxResults = 50, maxFiles = 20) {
|
|
12
|
+
if (!existsSync(ARCHIVE_DIR))
|
|
13
|
+
return { pattern, totalHits: 0, filesSearched: 0, hits: [] };
|
|
14
|
+
let regex;
|
|
15
|
+
try {
|
|
16
|
+
regex = new RegExp(pattern, "gi");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return { pattern, totalHits: 0, filesSearched: 0, hits: [] };
|
|
20
|
+
}
|
|
21
|
+
const files = readdirSync(ARCHIVE_DIR)
|
|
22
|
+
.filter((f) => f.endsWith(".txt.gz"))
|
|
23
|
+
.sort()
|
|
24
|
+
.reverse()
|
|
25
|
+
.slice(0, maxFiles);
|
|
26
|
+
const hits = [];
|
|
27
|
+
let totalHits = 0;
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
try {
|
|
30
|
+
const compressed = readFileSync(join(ARCHIVE_DIR, file));
|
|
31
|
+
const content = gunzipSync(compressed).toString("utf-8");
|
|
32
|
+
const lines = content.split("\n");
|
|
33
|
+
for (let i = 0; i < lines.length; i++) {
|
|
34
|
+
regex.lastIndex = 0;
|
|
35
|
+
const match = regex.exec(lines[i]);
|
|
36
|
+
if (match) {
|
|
37
|
+
totalHits++;
|
|
38
|
+
if (hits.length < maxResults) {
|
|
39
|
+
hits.push({
|
|
40
|
+
archive: file,
|
|
41
|
+
lineNumber: i + 1,
|
|
42
|
+
line: lines[i].slice(0, 200),
|
|
43
|
+
matchStart: match.index,
|
|
44
|
+
matchEnd: match.index + match[0].length,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch { /* skip unreadable archives */ }
|
|
51
|
+
}
|
|
52
|
+
return { pattern, totalHits, filesSearched: files.length, hits };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Format grep results for TUI display.
|
|
56
|
+
*/
|
|
57
|
+
export function formatGrepResult(result) {
|
|
58
|
+
if (result.totalHits === 0)
|
|
59
|
+
return [` fleet-grep: no matches for "${result.pattern}" in ${result.filesSearched} archives`];
|
|
60
|
+
const lines = [];
|
|
61
|
+
lines.push(` fleet-grep: ${result.totalHits} match${result.totalHits !== 1 ? "es" : ""} for "${result.pattern}" in ${result.filesSearched} archives:`);
|
|
62
|
+
for (const h of result.hits.slice(0, 15)) {
|
|
63
|
+
const preview = h.line.length > 80 ? h.line.slice(0, 77) + "..." : h.line;
|
|
64
|
+
lines.push(` [${h.archive.slice(0, 30)}:${h.lineNumber}] ${preview}`);
|
|
65
|
+
}
|
|
66
|
+
if (result.hits.length > 15)
|
|
67
|
+
lines.push(` ... and ${result.hits.length - 15} more`);
|
|
68
|
+
return lines;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=fleet-grep.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
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface HealthForecast {
|
|
2
|
+
currentHealth: number;
|
|
3
|
+
trendPerHour: number;
|
|
4
|
+
projectedHealth1h: number;
|
|
5
|
+
projectedHealth4h: number;
|
|
6
|
+
projectedHealth24h: number;
|
|
7
|
+
slaBreachInMs: number;
|
|
8
|
+
slaBreachLabel: string;
|
|
9
|
+
trend: "improving" | "stable" | "declining";
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Forecast fleet health from a time series of samples.
|
|
13
|
+
* Each sample is { timestamp, health }.
|
|
14
|
+
*/
|
|
15
|
+
export declare function forecastHealth(samples: Array<{
|
|
16
|
+
timestamp: number;
|
|
17
|
+
health: number;
|
|
18
|
+
}>, slaThreshold?: number, now?: number): HealthForecast | null;
|
|
19
|
+
/**
|
|
20
|
+
* Format health forecast for TUI display.
|
|
21
|
+
*/
|
|
22
|
+
export declare function formatHealthForecast(forecast: HealthForecast): string[];
|
|
23
|
+
//# sourceMappingURL=health-forecast.d.ts.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// health-forecast.ts — predict fleet health trends from historical data.
|
|
2
|
+
// uses linear regression on recent health samples to project future health
|
|
3
|
+
// and estimate when an SLA breach might occur.
|
|
4
|
+
/**
|
|
5
|
+
* Forecast fleet health from a time series of samples.
|
|
6
|
+
* Each sample is { timestamp, health }.
|
|
7
|
+
*/
|
|
8
|
+
export function forecastHealth(samples, slaThreshold = 50, now = Date.now()) {
|
|
9
|
+
if (samples.length < 3)
|
|
10
|
+
return null;
|
|
11
|
+
// simple linear regression: health = slope * time + intercept
|
|
12
|
+
const n = samples.length;
|
|
13
|
+
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
|
|
14
|
+
const t0 = samples[0].timestamp;
|
|
15
|
+
for (const s of samples) {
|
|
16
|
+
const x = (s.timestamp - t0) / 3_600_000; // hours since first sample
|
|
17
|
+
const y = s.health;
|
|
18
|
+
sumX += x;
|
|
19
|
+
sumY += y;
|
|
20
|
+
sumXY += x * y;
|
|
21
|
+
sumX2 += x * x;
|
|
22
|
+
}
|
|
23
|
+
const denom = n * sumX2 - sumX * sumX;
|
|
24
|
+
if (Math.abs(denom) < 0.001) {
|
|
25
|
+
// all samples at same time — can't compute trend
|
|
26
|
+
const current = samples[samples.length - 1].health;
|
|
27
|
+
return {
|
|
28
|
+
currentHealth: current,
|
|
29
|
+
trendPerHour: 0,
|
|
30
|
+
projectedHealth1h: current,
|
|
31
|
+
projectedHealth4h: current,
|
|
32
|
+
projectedHealth24h: current,
|
|
33
|
+
slaBreachInMs: current < slaThreshold ? 0 : -1,
|
|
34
|
+
slaBreachLabel: current < slaThreshold ? "now" : "never",
|
|
35
|
+
trend: "stable",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const slope = (n * sumXY - sumX * sumY) / denom;
|
|
39
|
+
const intercept = (sumY - slope * sumX) / n;
|
|
40
|
+
const currentHours = (now - t0) / 3_600_000;
|
|
41
|
+
const current = Math.round(intercept + slope * currentHours);
|
|
42
|
+
const h1 = Math.round(intercept + slope * (currentHours + 1));
|
|
43
|
+
const h4 = Math.round(intercept + slope * (currentHours + 4));
|
|
44
|
+
const h24 = Math.round(intercept + slope * (currentHours + 24));
|
|
45
|
+
// clamp projections to 0-100
|
|
46
|
+
const clamp = (v) => Math.max(0, Math.min(100, v));
|
|
47
|
+
let slaBreachMs = -1;
|
|
48
|
+
let slaBreachLabel = "never";
|
|
49
|
+
if (slope < -0.1 && current > slaThreshold) {
|
|
50
|
+
// health is declining — estimate when it crosses threshold
|
|
51
|
+
const hoursToBreak = (slaThreshold - current) / slope; // negative slope, so hoursToBreak > 0 when current > threshold
|
|
52
|
+
if (hoursToBreak > 0) {
|
|
53
|
+
slaBreachMs = Math.round(hoursToBreak * 3_600_000);
|
|
54
|
+
slaBreachLabel = formatDuration(slaBreachMs);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else if (current <= slaThreshold) {
|
|
58
|
+
slaBreachMs = 0;
|
|
59
|
+
slaBreachLabel = "now";
|
|
60
|
+
}
|
|
61
|
+
const trend = slope > 0.5 ? "improving" : slope < -0.5 ? "declining" : "stable";
|
|
62
|
+
return {
|
|
63
|
+
currentHealth: clamp(current),
|
|
64
|
+
trendPerHour: Math.round(slope * 10) / 10,
|
|
65
|
+
projectedHealth1h: clamp(h1),
|
|
66
|
+
projectedHealth4h: clamp(h4),
|
|
67
|
+
projectedHealth24h: clamp(h24),
|
|
68
|
+
slaBreachInMs: slaBreachMs,
|
|
69
|
+
slaBreachLabel,
|
|
70
|
+
trend,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Format health forecast for TUI display.
|
|
75
|
+
*/
|
|
76
|
+
export function formatHealthForecast(forecast) {
|
|
77
|
+
const trendIcon = forecast.trend === "improving" ? "📈" : forecast.trend === "declining" ? "📉" : "➡";
|
|
78
|
+
const lines = [];
|
|
79
|
+
lines.push(` ${trendIcon} Fleet health forecast (${forecast.trend}, ${forecast.trendPerHour > 0 ? "+" : ""}${forecast.trendPerHour}/hr):`);
|
|
80
|
+
lines.push(` Now: ${forecast.currentHealth}/100 → 1h: ${forecast.projectedHealth1h} 4h: ${forecast.projectedHealth4h} 24h: ${forecast.projectedHealth24h}`);
|
|
81
|
+
if (forecast.slaBreachInMs === 0) {
|
|
82
|
+
lines.push(` 🔴 SLA breach: NOW`);
|
|
83
|
+
}
|
|
84
|
+
else if (forecast.slaBreachInMs > 0) {
|
|
85
|
+
lines.push(` ⚠ SLA breach in: ${forecast.slaBreachLabel}`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
lines.push(` ✅ No SLA breach projected`);
|
|
89
|
+
}
|
|
90
|
+
return lines;
|
|
91
|
+
}
|
|
92
|
+
function formatDuration(ms) {
|
|
93
|
+
if (ms < 60_000)
|
|
94
|
+
return `${Math.round(ms / 1000)}s`;
|
|
95
|
+
if (ms < 3_600_000)
|
|
96
|
+
return `${Math.round(ms / 60_000)}m`;
|
|
97
|
+
const h = Math.floor(ms / 3_600_000);
|
|
98
|
+
const m = Math.round((ms % 3_600_000) / 60_000);
|
|
99
|
+
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=health-forecast.js.map
|