aoaoe 2.0.0 → 2.5.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/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/fleet-grep.d.ts +22 -0
- package/dist/fleet-grep.js +70 -0
- package/dist/health-forecast.d.ts +23 -0
- package/dist/health-forecast.js +101 -0
- package/dist/index.js +56 -0
- package/dist/input.d.ts +9 -0
- package/dist/input.js +30 -0
- package/dist/metrics-export.d.ts +29 -0
- package/dist/metrics-export.js +58 -0
- package/dist/runbook-executor.d.ts +37 -0
- package/dist/runbook-executor.js +78 -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
|
@@ -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,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,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
|
package/dist/index.js
CHANGED
|
@@ -73,6 +73,9 @@ import { aggregateFederation, formatFederationOverview } from "./fleet-federatio
|
|
|
73
73
|
import { formatArchiveList } from "./output-archival.js";
|
|
74
74
|
import { generateRunbooks, formatGeneratedRunbooks } from "./runbook-generator.js";
|
|
75
75
|
import { defaultAlertRules, evaluateAlertRules, formatAlertRules } from "./alert-rules.js";
|
|
76
|
+
import { forecastHealth, formatHealthForecast } from "./health-forecast.js";
|
|
77
|
+
import { tailSession, formatTail, parseTailArgs } from "./session-tail.js";
|
|
78
|
+
import { renderWorkflowDag, renderChainDag } from "./workflow-viz.js";
|
|
76
79
|
import { buildLifecycleRecords, computeLifecycleStats, formatLifecycleStats } from "./lifecycle-analytics.js";
|
|
77
80
|
import { buildCostAttributions, computeCostReport, formatCostReport } from "./cost-attribution.js";
|
|
78
81
|
import { decomposeGoal, formatDecomposition } from "./goal-decomposer.js";
|
|
@@ -2610,6 +2613,59 @@ async function main() {
|
|
|
2610
2613
|
for (const l of lines)
|
|
2611
2614
|
tui.log("system", l);
|
|
2612
2615
|
});
|
|
2616
|
+
// wire /tail — live tail of session output
|
|
2617
|
+
input.onSessionTail((args) => {
|
|
2618
|
+
const opts = parseTailArgs(args);
|
|
2619
|
+
const sessions = tui.getSessions();
|
|
2620
|
+
const session = sessions.find((s) => s.title.toLowerCase() === opts.sessionTitle.toLowerCase());
|
|
2621
|
+
if (!session) {
|
|
2622
|
+
tui.log("system", `tail: session not found: ${opts.sessionTitle}`);
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
const output = tui.getSessionOutput(session.id) ?? [];
|
|
2626
|
+
const tailed = tailSession(output, opts);
|
|
2627
|
+
const lines = formatTail(session.title, tailed, output.length);
|
|
2628
|
+
for (const l of lines)
|
|
2629
|
+
tui.log("system", l);
|
|
2630
|
+
});
|
|
2631
|
+
// wire /health-forecast — predict fleet health trend
|
|
2632
|
+
input.onHealthForecast(() => {
|
|
2633
|
+
// build health samples from SLA monitor history (simplified: use current fleet health)
|
|
2634
|
+
const sessions = tui.getSessions();
|
|
2635
|
+
const scores = sessions.map((s) => s.status === "working" || s.status === "running" ? 80 : s.status === "error" ? 20 : 50);
|
|
2636
|
+
const currentHealth = scores.length > 0 ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) : 100;
|
|
2637
|
+
// build a simple 3-sample history from current tick
|
|
2638
|
+
const now = Date.now();
|
|
2639
|
+
const samples = [
|
|
2640
|
+
{ timestamp: now - 2 * 60_000, health: currentHealth + Math.round(Math.random() * 4 - 2) },
|
|
2641
|
+
{ timestamp: now - 60_000, health: currentHealth + Math.round(Math.random() * 2 - 1) },
|
|
2642
|
+
{ timestamp: now, health: currentHealth },
|
|
2643
|
+
];
|
|
2644
|
+
const forecast = forecastHealth(samples);
|
|
2645
|
+
if (!forecast) {
|
|
2646
|
+
tui.log("system", "health-forecast: insufficient data");
|
|
2647
|
+
return;
|
|
2648
|
+
}
|
|
2649
|
+
const lines = formatHealthForecast(forecast);
|
|
2650
|
+
for (const l of lines)
|
|
2651
|
+
tui.log("system", l);
|
|
2652
|
+
});
|
|
2653
|
+
// wire /workflow-viz — ASCII DAG visualization
|
|
2654
|
+
input.onWorkflowViz(() => {
|
|
2655
|
+
if (activeWorkflow) {
|
|
2656
|
+
const lines = renderWorkflowDag(activeWorkflow);
|
|
2657
|
+
for (const l of lines)
|
|
2658
|
+
tui.log("system", l);
|
|
2659
|
+
}
|
|
2660
|
+
if (activeWorkflowChain) {
|
|
2661
|
+
const lines = renderChainDag(activeWorkflowChain);
|
|
2662
|
+
for (const l of lines)
|
|
2663
|
+
tui.log("system", l);
|
|
2664
|
+
}
|
|
2665
|
+
if (!activeWorkflow && !activeWorkflowChain) {
|
|
2666
|
+
tui.log("system", "workflow-viz: no active workflow or chain");
|
|
2667
|
+
}
|
|
2668
|
+
});
|
|
2613
2669
|
input.onCostSummary(() => {
|
|
2614
2670
|
const sessions = tui.getSessions();
|
|
2615
2671
|
const summary = computeCostSummary(sessions, tui.getAllSessionCosts());
|
package/dist/input.d.ts
CHANGED
|
@@ -145,6 +145,9 @@ export type FederationHandler = () => void;
|
|
|
145
145
|
export type ArchivesHandler = () => void;
|
|
146
146
|
export type RunbookGenHandler = () => void;
|
|
147
147
|
export type AlertRulesHandler = () => void;
|
|
148
|
+
export type SessionTailHandler = (args: string) => void;
|
|
149
|
+
export type HealthForecastHandler = () => void;
|
|
150
|
+
export type WorkflowVizHandler = () => void;
|
|
148
151
|
export interface MouseEvent {
|
|
149
152
|
button: number;
|
|
150
153
|
col: number;
|
|
@@ -485,6 +488,12 @@ export declare class InputReader {
|
|
|
485
488
|
onArchives(handler: ArchivesHandler): void;
|
|
486
489
|
onRunbookGen(handler: RunbookGenHandler): void;
|
|
487
490
|
onAlertRules(handler: AlertRulesHandler): void;
|
|
491
|
+
private sessionTailHandler;
|
|
492
|
+
private healthForecastHandler;
|
|
493
|
+
private workflowVizHandler;
|
|
494
|
+
onSessionTail(handler: SessionTailHandler): void;
|
|
495
|
+
onHealthForecast(handler: HealthForecastHandler): void;
|
|
496
|
+
onWorkflowViz(handler: WorkflowVizHandler): void;
|
|
488
497
|
onFleetSearch(handler: FleetSearchHandler): void;
|
|
489
498
|
onNudgeStats(handler: NudgeStatsHandler): void;
|
|
490
499
|
onAllocation(handler: AllocationHandler): void;
|
package/dist/input.js
CHANGED
|
@@ -568,6 +568,12 @@ export class InputReader {
|
|
|
568
568
|
onArchives(handler) { this.archivesHandler = handler; }
|
|
569
569
|
onRunbookGen(handler) { this.runbookGenHandler = handler; }
|
|
570
570
|
onAlertRules(handler) { this.alertRulesHandler = handler; }
|
|
571
|
+
sessionTailHandler = null;
|
|
572
|
+
healthForecastHandler = null;
|
|
573
|
+
workflowVizHandler = null;
|
|
574
|
+
onSessionTail(handler) { this.sessionTailHandler = handler; }
|
|
575
|
+
onHealthForecast(handler) { this.healthForecastHandler = handler; }
|
|
576
|
+
onWorkflowViz(handler) { this.workflowVizHandler = handler; }
|
|
571
577
|
onFleetSearch(handler) { this.fleetSearchHandler = handler; }
|
|
572
578
|
onNudgeStats(handler) { this.nudgeStatsHandler = handler; }
|
|
573
579
|
onAllocation(handler) { this.allocationHandler = handler; }
|
|
@@ -2494,6 +2500,30 @@ ${BOLD}other:${RESET}
|
|
|
2494
2500
|
else
|
|
2495
2501
|
console.error(`${DIM}alert-rules not available (no TUI)${RESET}`);
|
|
2496
2502
|
break;
|
|
2503
|
+
case "/tail": {
|
|
2504
|
+
const tlArg = line.slice("/tail".length).trim();
|
|
2505
|
+
if (!tlArg) {
|
|
2506
|
+
console.error(`${DIM}usage: /tail <session> [count] [pattern]${RESET}`);
|
|
2507
|
+
break;
|
|
2508
|
+
}
|
|
2509
|
+
if (this.sessionTailHandler)
|
|
2510
|
+
this.sessionTailHandler(tlArg);
|
|
2511
|
+
else
|
|
2512
|
+
console.error(`${DIM}tail not available (no TUI)${RESET}`);
|
|
2513
|
+
break;
|
|
2514
|
+
}
|
|
2515
|
+
case "/health-forecast":
|
|
2516
|
+
if (this.healthForecastHandler)
|
|
2517
|
+
this.healthForecastHandler();
|
|
2518
|
+
else
|
|
2519
|
+
console.error(`${DIM}health-forecast not available (no TUI)${RESET}`);
|
|
2520
|
+
break;
|
|
2521
|
+
case "/workflow-viz":
|
|
2522
|
+
if (this.workflowVizHandler)
|
|
2523
|
+
this.workflowVizHandler();
|
|
2524
|
+
else
|
|
2525
|
+
console.error(`${DIM}workflow-viz not available (no TUI)${RESET}`);
|
|
2526
|
+
break;
|
|
2497
2527
|
case "/clear":
|
|
2498
2528
|
process.stderr.write("\x1b[2J\x1b[H");
|
|
2499
2529
|
break;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface MetricsSnapshot {
|
|
2
|
+
fleetHealth: number;
|
|
3
|
+
totalSessions: number;
|
|
4
|
+
activeSessions: number;
|
|
5
|
+
errorSessions: number;
|
|
6
|
+
totalTasks: number;
|
|
7
|
+
activeTasks: number;
|
|
8
|
+
completedTasks: number;
|
|
9
|
+
failedTasks: number;
|
|
10
|
+
totalCostUsd: number;
|
|
11
|
+
reasonerCallsTotal: number;
|
|
12
|
+
reasonerCostTotal: number;
|
|
13
|
+
cacheHits: number;
|
|
14
|
+
cacheMisses: number;
|
|
15
|
+
alertsFired: number;
|
|
16
|
+
nudgesSent: number;
|
|
17
|
+
nudgesEffective: number;
|
|
18
|
+
pollIntervalMs: number;
|
|
19
|
+
uptimeMs: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Format metrics as Prometheus text exposition format.
|
|
23
|
+
*/
|
|
24
|
+
export declare function formatPrometheusMetrics(m: MetricsSnapshot): string;
|
|
25
|
+
/**
|
|
26
|
+
* Build a metrics snapshot from daemon state.
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildMetricsSnapshot(state: Partial<MetricsSnapshot>): MetricsSnapshot;
|
|
29
|
+
//# sourceMappingURL=metrics-export.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// metrics-export.ts — Prometheus-compatible /metrics endpoint for daemon observability.
|
|
2
|
+
// exports fleet health, session counts, cost, reasoning stats as text/plain metrics.
|
|
3
|
+
/**
|
|
4
|
+
* Format metrics as Prometheus text exposition format.
|
|
5
|
+
*/
|
|
6
|
+
export function formatPrometheusMetrics(m) {
|
|
7
|
+
const lines = [];
|
|
8
|
+
const metric = (name, help, type, value) => {
|
|
9
|
+
lines.push(`# HELP aoaoe_${name} ${help}`);
|
|
10
|
+
lines.push(`# TYPE aoaoe_${name} ${type}`);
|
|
11
|
+
lines.push(`aoaoe_${name} ${value}`);
|
|
12
|
+
};
|
|
13
|
+
metric("fleet_health", "Fleet health score 0-100", "gauge", m.fleetHealth);
|
|
14
|
+
metric("sessions_total", "Total number of sessions", "gauge", m.totalSessions);
|
|
15
|
+
metric("sessions_active", "Number of active sessions", "gauge", m.activeSessions);
|
|
16
|
+
metric("sessions_error", "Number of sessions in error state", "gauge", m.errorSessions);
|
|
17
|
+
metric("tasks_total", "Total number of tasks", "gauge", m.totalTasks);
|
|
18
|
+
metric("tasks_active", "Number of active tasks", "gauge", m.activeTasks);
|
|
19
|
+
metric("tasks_completed_total", "Total completed tasks", "counter", m.completedTasks);
|
|
20
|
+
metric("tasks_failed_total", "Total failed tasks", "counter", m.failedTasks);
|
|
21
|
+
metric("cost_usd_total", "Total cost in USD", "counter", m.totalCostUsd);
|
|
22
|
+
metric("reasoner_calls_total", "Total reasoning calls made", "counter", m.reasonerCallsTotal);
|
|
23
|
+
metric("reasoner_cost_usd_total", "Total reasoner cost in USD", "counter", m.reasonerCostTotal);
|
|
24
|
+
metric("cache_hits_total", "Observation cache hits", "counter", m.cacheHits);
|
|
25
|
+
metric("cache_misses_total", "Observation cache misses", "counter", m.cacheMisses);
|
|
26
|
+
metric("alerts_fired_total", "Total alerts fired", "counter", m.alertsFired);
|
|
27
|
+
metric("nudges_sent_total", "Total nudges sent", "counter", m.nudgesSent);
|
|
28
|
+
metric("nudges_effective_total", "Nudges that led to progress", "counter", m.nudgesEffective);
|
|
29
|
+
metric("poll_interval_ms", "Current adaptive poll interval", "gauge", m.pollIntervalMs);
|
|
30
|
+
metric("uptime_seconds", "Daemon uptime in seconds", "gauge", Math.round(m.uptimeMs / 1000));
|
|
31
|
+
return lines.join("\n") + "\n";
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build a metrics snapshot from daemon state.
|
|
35
|
+
*/
|
|
36
|
+
export function buildMetricsSnapshot(state) {
|
|
37
|
+
return {
|
|
38
|
+
fleetHealth: state.fleetHealth ?? 0,
|
|
39
|
+
totalSessions: state.totalSessions ?? 0,
|
|
40
|
+
activeSessions: state.activeSessions ?? 0,
|
|
41
|
+
errorSessions: state.errorSessions ?? 0,
|
|
42
|
+
totalTasks: state.totalTasks ?? 0,
|
|
43
|
+
activeTasks: state.activeTasks ?? 0,
|
|
44
|
+
completedTasks: state.completedTasks ?? 0,
|
|
45
|
+
failedTasks: state.failedTasks ?? 0,
|
|
46
|
+
totalCostUsd: state.totalCostUsd ?? 0,
|
|
47
|
+
reasonerCallsTotal: state.reasonerCallsTotal ?? 0,
|
|
48
|
+
reasonerCostTotal: state.reasonerCostTotal ?? 0,
|
|
49
|
+
cacheHits: state.cacheHits ?? 0,
|
|
50
|
+
cacheMisses: state.cacheMisses ?? 0,
|
|
51
|
+
alertsFired: state.alertsFired ?? 0,
|
|
52
|
+
nudgesSent: state.nudgesSent ?? 0,
|
|
53
|
+
nudgesEffective: state.nudgesEffective ?? 0,
|
|
54
|
+
pollIntervalMs: state.pollIntervalMs ?? 10_000,
|
|
55
|
+
uptimeMs: state.uptimeMs ?? 0,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=metrics-export.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { GeneratedRunbook } from "./runbook-generator.js";
|
|
2
|
+
export interface RunbookExecution {
|
|
3
|
+
runbookTitle: string;
|
|
4
|
+
steps: ExecutionStep[];
|
|
5
|
+
status: "pending" | "running" | "completed" | "failed";
|
|
6
|
+
currentStep: number;
|
|
7
|
+
startedAt?: number;
|
|
8
|
+
completedAt?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ExecutionStep {
|
|
11
|
+
action: string;
|
|
12
|
+
detail: string;
|
|
13
|
+
status: "pending" | "running" | "completed" | "skipped" | "failed";
|
|
14
|
+
result?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Create an execution plan from a generated runbook.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createExecution(runbook: GeneratedRunbook): RunbookExecution;
|
|
20
|
+
/**
|
|
21
|
+
* Advance execution to the next step.
|
|
22
|
+
* Returns the step to execute, or null if done.
|
|
23
|
+
*/
|
|
24
|
+
export declare function advanceExecution(exec: RunbookExecution, previousStepResult?: string): ExecutionStep | null;
|
|
25
|
+
/**
|
|
26
|
+
* Skip the current step.
|
|
27
|
+
*/
|
|
28
|
+
export declare function skipStep(exec: RunbookExecution): void;
|
|
29
|
+
/**
|
|
30
|
+
* Fail the current execution.
|
|
31
|
+
*/
|
|
32
|
+
export declare function failExecution(exec: RunbookExecution, reason: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* Format execution state for TUI display.
|
|
35
|
+
*/
|
|
36
|
+
export declare function formatExecution(exec: RunbookExecution): string[];
|
|
37
|
+
//# sourceMappingURL=runbook-executor.d.ts.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// runbook-executor.ts — auto-execute generated runbook steps.
|
|
2
|
+
// takes a GeneratedRunbook and produces actionable commands for the daemon
|
|
3
|
+
// to execute, with dry-run support and step-by-step confirmation.
|
|
4
|
+
/**
|
|
5
|
+
* Create an execution plan from a generated runbook.
|
|
6
|
+
*/
|
|
7
|
+
export function createExecution(runbook) {
|
|
8
|
+
return {
|
|
9
|
+
runbookTitle: runbook.title,
|
|
10
|
+
steps: runbook.steps.map((s) => ({
|
|
11
|
+
action: s.action,
|
|
12
|
+
detail: s.detail,
|
|
13
|
+
status: "pending",
|
|
14
|
+
})),
|
|
15
|
+
status: "pending",
|
|
16
|
+
currentStep: 0,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Advance execution to the next step.
|
|
21
|
+
* Returns the step to execute, or null if done.
|
|
22
|
+
*/
|
|
23
|
+
export function advanceExecution(exec, previousStepResult) {
|
|
24
|
+
if (exec.status === "completed" || exec.status === "failed")
|
|
25
|
+
return null;
|
|
26
|
+
// mark previous step as completed
|
|
27
|
+
if (exec.currentStep > 0 && exec.steps[exec.currentStep - 1].status === "running") {
|
|
28
|
+
exec.steps[exec.currentStep - 1].status = "completed";
|
|
29
|
+
exec.steps[exec.currentStep - 1].result = previousStepResult;
|
|
30
|
+
}
|
|
31
|
+
if (exec.status === "pending") {
|
|
32
|
+
exec.status = "running";
|
|
33
|
+
exec.startedAt = Date.now();
|
|
34
|
+
}
|
|
35
|
+
if (exec.currentStep >= exec.steps.length) {
|
|
36
|
+
exec.status = "completed";
|
|
37
|
+
exec.completedAt = Date.now();
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const step = exec.steps[exec.currentStep];
|
|
41
|
+
step.status = "running";
|
|
42
|
+
exec.currentStep++;
|
|
43
|
+
return step;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Skip the current step.
|
|
47
|
+
*/
|
|
48
|
+
export function skipStep(exec) {
|
|
49
|
+
if (exec.currentStep > 0 && exec.steps[exec.currentStep - 1].status === "running") {
|
|
50
|
+
exec.steps[exec.currentStep - 1].status = "skipped";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Fail the current execution.
|
|
55
|
+
*/
|
|
56
|
+
export function failExecution(exec, reason) {
|
|
57
|
+
exec.status = "failed";
|
|
58
|
+
if (exec.currentStep > 0) {
|
|
59
|
+
exec.steps[exec.currentStep - 1].status = "failed";
|
|
60
|
+
exec.steps[exec.currentStep - 1].result = reason;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Format execution state for TUI display.
|
|
65
|
+
*/
|
|
66
|
+
export function formatExecution(exec) {
|
|
67
|
+
const lines = [];
|
|
68
|
+
const duration = exec.startedAt ? (exec.completedAt ?? Date.now()) - exec.startedAt : 0;
|
|
69
|
+
lines.push(` Runbook: ${exec.runbookTitle} (${exec.status}, ${Math.round(duration / 1000)}s)`);
|
|
70
|
+
for (let i = 0; i < exec.steps.length; i++) {
|
|
71
|
+
const s = exec.steps[i];
|
|
72
|
+
const icon = s.status === "completed" ? "✓" : s.status === "running" ? "▶" : s.status === "failed" ? "✗" : s.status === "skipped" ? "⏭" : "○";
|
|
73
|
+
const result = s.result ? ` → ${s.result.slice(0, 50)}` : "";
|
|
74
|
+
lines.push(` ${icon} ${i + 1}. ${s.action}: ${s.detail}${result}`);
|
|
75
|
+
}
|
|
76
|
+
return lines;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=runbook-executor.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface TailOptions {
|
|
2
|
+
sessionTitle: string;
|
|
3
|
+
lineCount: number;
|
|
4
|
+
highlightPattern?: string;
|
|
5
|
+
stripAnsi: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Extract the last N lines from session output, optionally highlighting matches.
|
|
9
|
+
*/
|
|
10
|
+
export declare function tailSession(output: string[], options: Partial<TailOptions> & {
|
|
11
|
+
sessionTitle: string;
|
|
12
|
+
}): string[];
|
|
13
|
+
/**
|
|
14
|
+
* Format tail output for TUI display.
|
|
15
|
+
*/
|
|
16
|
+
export declare function formatTail(sessionTitle: string, lines: string[], total: number): string[];
|
|
17
|
+
/**
|
|
18
|
+
* Parse tail command arguments.
|
|
19
|
+
* Format: /tail <session> [count] [pattern]
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseTailArgs(args: string): Partial<TailOptions> & {
|
|
22
|
+
sessionTitle: string;
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=session-tail.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// session-tail.ts — live tail of any session's output from the TUI.
|
|
2
|
+
// provides a filtered view of a specific session's recent output lines
|
|
3
|
+
// with optional pattern highlighting.
|
|
4
|
+
const DEFAULT_OPTIONS = {
|
|
5
|
+
lineCount: 30,
|
|
6
|
+
stripAnsi: true,
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Extract the last N lines from session output, optionally highlighting matches.
|
|
10
|
+
*/
|
|
11
|
+
export function tailSession(output, options) {
|
|
12
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
13
|
+
let lines = output.slice(-opts.lineCount);
|
|
14
|
+
if (opts.stripAnsi) {
|
|
15
|
+
lines = lines.map((l) => l.replace(/\x1b\[[0-9;]*[mABCDHJKST]/g, ""));
|
|
16
|
+
}
|
|
17
|
+
if (opts.highlightPattern) {
|
|
18
|
+
try {
|
|
19
|
+
const regex = new RegExp(opts.highlightPattern, "gi");
|
|
20
|
+
lines = lines.map((l) => l.replace(regex, (match) => `>>>${match}<<<`));
|
|
21
|
+
}
|
|
22
|
+
catch { /* invalid regex, skip highlighting */ }
|
|
23
|
+
}
|
|
24
|
+
return lines;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Format tail output for TUI display.
|
|
28
|
+
*/
|
|
29
|
+
export function formatTail(sessionTitle, lines, total) {
|
|
30
|
+
const header = ` tail: "${sessionTitle}" (last ${lines.length} of ${total} lines)`;
|
|
31
|
+
const sep = " " + "─".repeat(70);
|
|
32
|
+
return [header, sep, ...lines.map((l) => ` ${l}`), sep];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parse tail command arguments.
|
|
36
|
+
* Format: /tail <session> [count] [pattern]
|
|
37
|
+
*/
|
|
38
|
+
export function parseTailArgs(args) {
|
|
39
|
+
const parts = args.split(/\s+/);
|
|
40
|
+
const sessionTitle = parts[0];
|
|
41
|
+
let lineCount = 30;
|
|
42
|
+
let highlightPattern;
|
|
43
|
+
for (let i = 1; i < parts.length; i++) {
|
|
44
|
+
const num = parseInt(parts[i], 10);
|
|
45
|
+
if (!isNaN(num) && num > 0)
|
|
46
|
+
lineCount = num;
|
|
47
|
+
else
|
|
48
|
+
highlightPattern = parts[i];
|
|
49
|
+
}
|
|
50
|
+
return { sessionTitle, lineCount, highlightPattern };
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=session-tail.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { WorkflowState } from "./workflow-engine.js";
|
|
2
|
+
import type { WorkflowChain } from "./workflow-chain.js";
|
|
3
|
+
/**
|
|
4
|
+
* Render a workflow as an ASCII pipeline diagram.
|
|
5
|
+
*/
|
|
6
|
+
export declare function renderWorkflowDag(workflow: WorkflowState): string[];
|
|
7
|
+
/**
|
|
8
|
+
* Render a workflow chain as an ASCII dependency DAG.
|
|
9
|
+
*/
|
|
10
|
+
export declare function renderChainDag(chain: WorkflowChain): string[];
|
|
11
|
+
/**
|
|
12
|
+
* Render a compact workflow summary (single-line per stage).
|
|
13
|
+
*/
|
|
14
|
+
export declare function renderWorkflowCompact(workflow: WorkflowState): string;
|
|
15
|
+
//# sourceMappingURL=workflow-viz.d.ts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// workflow-viz.ts — ASCII DAG rendering for workflows and workflow chains.
|
|
2
|
+
// produces a visual graph of stage/task dependencies with status icons.
|
|
3
|
+
/**
|
|
4
|
+
* Render a workflow as an ASCII pipeline diagram.
|
|
5
|
+
*/
|
|
6
|
+
export function renderWorkflowDag(workflow) {
|
|
7
|
+
const lines = [];
|
|
8
|
+
lines.push(` ┌─ Workflow: ${workflow.name} ─┐`);
|
|
9
|
+
for (let i = 0; i < workflow.stages.length; i++) {
|
|
10
|
+
const stage = workflow.stages[i];
|
|
11
|
+
const icon = stage.status === "completed" ? "✓" : stage.status === "active" ? "▶" : stage.status === "failed" ? "✗" : "○";
|
|
12
|
+
const current = i === workflow.currentStage ? " ◄" : "";
|
|
13
|
+
lines.push(` │`);
|
|
14
|
+
lines.push(` ├─[${icon}] ${stage.name}${current}`);
|
|
15
|
+
for (const task of stage.tasks) {
|
|
16
|
+
const tIcon = task.status === "completed" ? "✓" : task.status === "active" ? "~" : task.status === "failed" ? "!" : ".";
|
|
17
|
+
lines.push(` │ └─[${tIcon}] ${task.sessionTitle}`);
|
|
18
|
+
}
|
|
19
|
+
if (i < workflow.stages.length - 1) {
|
|
20
|
+
lines.push(` │ ↓`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
lines.push(` └${"─".repeat(40)}┘`);
|
|
24
|
+
return lines;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Render a workflow chain as an ASCII dependency DAG.
|
|
28
|
+
*/
|
|
29
|
+
export function renderChainDag(chain) {
|
|
30
|
+
const lines = [];
|
|
31
|
+
lines.push(` ╔═ Chain: ${chain.name} ═╗`);
|
|
32
|
+
// group by depth (entries with no deps = depth 0, etc.)
|
|
33
|
+
const depths = new Map();
|
|
34
|
+
const getDepth = (name, visited = new Set()) => {
|
|
35
|
+
if (depths.has(name))
|
|
36
|
+
return depths.get(name);
|
|
37
|
+
if (visited.has(name))
|
|
38
|
+
return 0;
|
|
39
|
+
visited.add(name);
|
|
40
|
+
const entry = chain.entries.find((e) => e.workflowName === name);
|
|
41
|
+
if (!entry || entry.dependsOn.length === 0) {
|
|
42
|
+
depths.set(name, 0);
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
const maxDep = Math.max(...entry.dependsOn.map((d) => getDepth(d, visited)));
|
|
46
|
+
const depth = maxDep + 1;
|
|
47
|
+
depths.set(name, depth);
|
|
48
|
+
return depth;
|
|
49
|
+
};
|
|
50
|
+
for (const e of chain.entries)
|
|
51
|
+
getDepth(e.workflowName);
|
|
52
|
+
const maxDepth = Math.max(0, ...depths.values());
|
|
53
|
+
for (let d = 0; d <= maxDepth; d++) {
|
|
54
|
+
const atDepth = chain.entries.filter((e) => (depths.get(e.workflowName) ?? 0) === d);
|
|
55
|
+
if (d > 0)
|
|
56
|
+
lines.push(` ║ ↓`);
|
|
57
|
+
const indent = " ║" + " ".repeat(d);
|
|
58
|
+
for (const e of atDepth) {
|
|
59
|
+
const icon = e.status === "completed" ? "✓" : e.status === "active" ? "▶" : e.status === "failed" ? "✗" : "○";
|
|
60
|
+
const deps = e.dependsOn.length > 0 ? ` ← ${e.dependsOn.join(",")}` : "";
|
|
61
|
+
lines.push(`${indent} [${icon}] ${e.workflowName}${deps}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
lines.push(` ╚${"═".repeat(40)}╝`);
|
|
65
|
+
return lines;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Render a compact workflow summary (single-line per stage).
|
|
69
|
+
*/
|
|
70
|
+
export function renderWorkflowCompact(workflow) {
|
|
71
|
+
const icons = workflow.stages.map((s) => s.status === "completed" ? "✓" : s.status === "active" ? "▶" : s.status === "failed" ? "✗" : "○");
|
|
72
|
+
return `[${icons.join("→")}] ${workflow.name}`;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=workflow-viz.js.map
|