aoaoe 1.0.0 → 2.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 +23 -1
- package/dist/ab-reasoning.d.ts +42 -0
- package/dist/ab-reasoning.js +91 -0
- package/dist/alert-rules.d.ts +42 -0
- package/dist/alert-rules.js +94 -0
- package/dist/cli-completions.d.ts +24 -0
- package/dist/cli-completions.js +114 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +8 -1
- package/dist/fleet-federation.d.ts +34 -0
- package/dist/fleet-federation.js +55 -0
- package/dist/index.js +281 -1
- package/dist/input.d.ts +42 -0
- package/dist/input.js +130 -0
- package/dist/multi-reasoner.d.ts +36 -0
- package/dist/multi-reasoner.js +87 -0
- package/dist/output-archival.d.ts +23 -0
- package/dist/output-archival.js +72 -0
- package/dist/runbook-generator.d.ts +21 -0
- package/dist/runbook-generator.js +104 -0
- package/dist/service-generator.d.ts +32 -0
- package/dist/service-generator.js +132 -0
- package/dist/session-checkpoint.d.ts +55 -0
- package/dist/session-checkpoint.js +69 -0
- package/dist/session-replay.d.ts +25 -0
- package/dist/session-replay.js +103 -0
- package/dist/token-quota.d.ts +45 -0
- package/dist/token-quota.js +76 -0
- package/dist/workflow-chain.d.ts +33 -0
- package/dist/workflow-chain.js +69 -0
- package/dist/workflow-cost-forecast.d.ts +22 -0
- package/dist/workflow-cost-forecast.js +55 -0
- package/dist/workflow-engine.d.ts +48 -0
- package/dist/workflow-engine.js +91 -0
- package/dist/workflow-templates.d.ts +25 -0
- package/dist/workflow-templates.js +92 -0
- package/package.json +1 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// session-replay.ts — replay a session's activity timeline step-by-step.
|
|
2
|
+
// reconstructs the session's history from audit trail + fleet snapshots
|
|
3
|
+
// and presents it as a chronological narrative for post-mortem analysis.
|
|
4
|
+
import { readRecentAuditEntries } from "./audit-trail.js";
|
|
5
|
+
/**
|
|
6
|
+
* Build a replay timeline for a session from the audit trail.
|
|
7
|
+
*/
|
|
8
|
+
export function buildSessionReplay(sessionTitle, maxEvents = 200) {
|
|
9
|
+
const all = readRecentAuditEntries(10_000);
|
|
10
|
+
const sessionEvents = all.filter((e) => e.session === sessionTitle ||
|
|
11
|
+
e.detail.toLowerCase().includes(sessionTitle.toLowerCase()));
|
|
12
|
+
const events = sessionEvents.slice(-maxEvents).map((e) => ({
|
|
13
|
+
timestamp: new Date(e.timestamp).getTime(),
|
|
14
|
+
timeLabel: e.timestamp.slice(11, 19),
|
|
15
|
+
type: e.type,
|
|
16
|
+
detail: e.detail,
|
|
17
|
+
}));
|
|
18
|
+
const totalDuration = events.length >= 2
|
|
19
|
+
? events[events.length - 1].timestamp - events[0].timestamp
|
|
20
|
+
: 0;
|
|
21
|
+
return {
|
|
22
|
+
sessionTitle,
|
|
23
|
+
events,
|
|
24
|
+
totalDurationMs: totalDuration,
|
|
25
|
+
eventCount: events.length,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Format replay as a chronological narrative.
|
|
30
|
+
*/
|
|
31
|
+
export function formatReplay(replay) {
|
|
32
|
+
if (replay.events.length === 0) {
|
|
33
|
+
return [` No replay data for "${replay.sessionTitle}" (check audit trail)`];
|
|
34
|
+
}
|
|
35
|
+
const lines = [];
|
|
36
|
+
const durationLabel = replay.totalDurationMs > 0
|
|
37
|
+
? formatDuration(replay.totalDurationMs)
|
|
38
|
+
: "instant";
|
|
39
|
+
lines.push(` Replay: "${replay.sessionTitle}" (${replay.eventCount} events, ${durationLabel})`);
|
|
40
|
+
lines.push("");
|
|
41
|
+
let lastTimestamp = 0;
|
|
42
|
+
for (const e of replay.events) {
|
|
43
|
+
// show time gap between events
|
|
44
|
+
if (lastTimestamp > 0) {
|
|
45
|
+
const gap = e.timestamp - lastTimestamp;
|
|
46
|
+
if (gap > 60_000) {
|
|
47
|
+
lines.push(` ... ${formatDuration(gap)} later ...`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
lastTimestamp = e.timestamp;
|
|
51
|
+
const icon = eventIcon(e.type);
|
|
52
|
+
lines.push(` ${e.timeLabel} ${icon} ${e.type}: ${e.detail}`);
|
|
53
|
+
}
|
|
54
|
+
return lines;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Generate a summary of the replay (for quick review).
|
|
58
|
+
*/
|
|
59
|
+
export function summarizeReplay(replay) {
|
|
60
|
+
if (replay.events.length === 0)
|
|
61
|
+
return [` "${replay.sessionTitle}": no events`];
|
|
62
|
+
// count by type
|
|
63
|
+
const typeCounts = new Map();
|
|
64
|
+
for (const e of replay.events) {
|
|
65
|
+
typeCounts.set(e.type, (typeCounts.get(e.type) ?? 0) + 1);
|
|
66
|
+
}
|
|
67
|
+
const lines = [];
|
|
68
|
+
lines.push(` "${replay.sessionTitle}" summary:`);
|
|
69
|
+
lines.push(` Duration: ${formatDuration(replay.totalDurationMs)} Events: ${replay.eventCount}`);
|
|
70
|
+
const sorted = [...typeCounts.entries()].sort((a, b) => b[1] - a[1]);
|
|
71
|
+
for (const [type, count] of sorted) {
|
|
72
|
+
lines.push(` ${eventIcon(type)} ${type}: ${count}`);
|
|
73
|
+
}
|
|
74
|
+
return lines;
|
|
75
|
+
}
|
|
76
|
+
function eventIcon(type) {
|
|
77
|
+
const icons = {
|
|
78
|
+
reasoner_action: "🧠",
|
|
79
|
+
auto_complete: "✅",
|
|
80
|
+
budget_pause: "💰",
|
|
81
|
+
conflict_detected: "⚔",
|
|
82
|
+
operator_command: "👤",
|
|
83
|
+
task_created: "📋",
|
|
84
|
+
task_completed: "🏁",
|
|
85
|
+
session_error: "❌",
|
|
86
|
+
session_restart: "🔄",
|
|
87
|
+
config_change: "⚙",
|
|
88
|
+
stuck_nudge: "👉",
|
|
89
|
+
daemon_start: "🚀",
|
|
90
|
+
daemon_stop: "🛑",
|
|
91
|
+
};
|
|
92
|
+
return icons[type] ?? "·";
|
|
93
|
+
}
|
|
94
|
+
function formatDuration(ms) {
|
|
95
|
+
if (ms < 60_000)
|
|
96
|
+
return `${Math.round(ms / 1000)}s`;
|
|
97
|
+
if (ms < 3_600_000)
|
|
98
|
+
return `${Math.round(ms / 60_000)}m`;
|
|
99
|
+
const h = Math.floor(ms / 3_600_000);
|
|
100
|
+
const m = Math.round((ms % 3_600_000) / 60_000);
|
|
101
|
+
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=session-replay.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface TokenQuota {
|
|
2
|
+
model: string;
|
|
3
|
+
maxInputTokensPerHour: number;
|
|
4
|
+
maxOutputTokensPerHour: number;
|
|
5
|
+
}
|
|
6
|
+
export interface TokenUsageSample {
|
|
7
|
+
timestamp: number;
|
|
8
|
+
model: string;
|
|
9
|
+
inputTokens: number;
|
|
10
|
+
outputTokens: number;
|
|
11
|
+
}
|
|
12
|
+
export interface TokenQuotaStatus {
|
|
13
|
+
model: string;
|
|
14
|
+
inputTokensUsed: number;
|
|
15
|
+
outputTokensUsed: number;
|
|
16
|
+
inputLimit: number;
|
|
17
|
+
outputLimit: number;
|
|
18
|
+
inputPercent: number;
|
|
19
|
+
outputPercent: number;
|
|
20
|
+
blocked: boolean;
|
|
21
|
+
reason: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Manage per-model token quotas.
|
|
25
|
+
*/
|
|
26
|
+
export declare class TokenQuotaManager {
|
|
27
|
+
private quotas;
|
|
28
|
+
private samples;
|
|
29
|
+
private windowMs;
|
|
30
|
+
constructor(quotas?: TokenQuota[], windowMs?: number);
|
|
31
|
+
/** Set or update a quota for a model. */
|
|
32
|
+
setQuota(model: string, maxInput: number, maxOutput: number): void;
|
|
33
|
+
/** Record token usage for a model. */
|
|
34
|
+
recordUsage(model: string, inputTokens: number, outputTokens: number, now?: number): void;
|
|
35
|
+
/** Check if a model is within its token quota. */
|
|
36
|
+
getStatus(model: string, now?: number): TokenQuotaStatus;
|
|
37
|
+
/** Check if reasoning should be blocked for a model. */
|
|
38
|
+
isBlocked(model: string, now?: number): boolean;
|
|
39
|
+
/** Get status for all models with quotas. */
|
|
40
|
+
getAllStatuses(now?: number): TokenQuotaStatus[];
|
|
41
|
+
/** Format for TUI display. */
|
|
42
|
+
formatAll(now?: number): string[];
|
|
43
|
+
private prune;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=token-quota.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// token-quota.ts — per-model token quotas for fleet-wide rate limiting.
|
|
2
|
+
// extends the USD-based rate limiter with token-level granularity per model.
|
|
3
|
+
/**
|
|
4
|
+
* Manage per-model token quotas.
|
|
5
|
+
*/
|
|
6
|
+
export class TokenQuotaManager {
|
|
7
|
+
quotas;
|
|
8
|
+
samples = [];
|
|
9
|
+
windowMs;
|
|
10
|
+
constructor(quotas = [], windowMs = 3_600_000) {
|
|
11
|
+
this.quotas = new Map(quotas.map((q) => [q.model, q]));
|
|
12
|
+
this.windowMs = windowMs;
|
|
13
|
+
}
|
|
14
|
+
/** Set or update a quota for a model. */
|
|
15
|
+
setQuota(model, maxInput, maxOutput) {
|
|
16
|
+
this.quotas.set(model, { model, maxInputTokensPerHour: maxInput, maxOutputTokensPerHour: maxOutput });
|
|
17
|
+
}
|
|
18
|
+
/** Record token usage for a model. */
|
|
19
|
+
recordUsage(model, inputTokens, outputTokens, now = Date.now()) {
|
|
20
|
+
this.samples.push({ timestamp: now, model, inputTokens, outputTokens });
|
|
21
|
+
this.prune(now);
|
|
22
|
+
}
|
|
23
|
+
/** Check if a model is within its token quota. */
|
|
24
|
+
getStatus(model, now = Date.now()) {
|
|
25
|
+
this.prune(now);
|
|
26
|
+
const quota = this.quotas.get(model);
|
|
27
|
+
if (!quota) {
|
|
28
|
+
return { model, inputTokensUsed: 0, outputTokensUsed: 0, inputLimit: Infinity, outputLimit: Infinity, inputPercent: 0, outputPercent: 0, blocked: false, reason: "no quota set" };
|
|
29
|
+
}
|
|
30
|
+
const cutoff = now - this.windowMs;
|
|
31
|
+
const modelSamples = this.samples.filter((s) => s.model === model && s.timestamp >= cutoff);
|
|
32
|
+
const inputUsed = modelSamples.reduce((sum, s) => sum + s.inputTokens, 0);
|
|
33
|
+
const outputUsed = modelSamples.reduce((sum, s) => sum + s.outputTokens, 0);
|
|
34
|
+
const inputPct = (inputUsed / quota.maxInputTokensPerHour) * 100;
|
|
35
|
+
const outputPct = (outputUsed / quota.maxOutputTokensPerHour) * 100;
|
|
36
|
+
const blocked = inputUsed >= quota.maxInputTokensPerHour || outputUsed >= quota.maxOutputTokensPerHour;
|
|
37
|
+
return {
|
|
38
|
+
model,
|
|
39
|
+
inputTokensUsed: inputUsed,
|
|
40
|
+
outputTokensUsed: outputUsed,
|
|
41
|
+
inputLimit: quota.maxInputTokensPerHour,
|
|
42
|
+
outputLimit: quota.maxOutputTokensPerHour,
|
|
43
|
+
inputPercent: inputPct,
|
|
44
|
+
outputPercent: outputPct,
|
|
45
|
+
blocked,
|
|
46
|
+
reason: blocked
|
|
47
|
+
? `${inputUsed >= quota.maxInputTokensPerHour ? "input" : "output"} quota exceeded`
|
|
48
|
+
: "ok",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Check if reasoning should be blocked for a model. */
|
|
52
|
+
isBlocked(model, now = Date.now()) {
|
|
53
|
+
return this.getStatus(model, now).blocked;
|
|
54
|
+
}
|
|
55
|
+
/** Get status for all models with quotas. */
|
|
56
|
+
getAllStatuses(now = Date.now()) {
|
|
57
|
+
return [...this.quotas.keys()].map((m) => this.getStatus(m, now));
|
|
58
|
+
}
|
|
59
|
+
/** Format for TUI display. */
|
|
60
|
+
formatAll(now = Date.now()) {
|
|
61
|
+
const statuses = this.getAllStatuses(now);
|
|
62
|
+
if (statuses.length === 0)
|
|
63
|
+
return [" (no token quotas configured)"];
|
|
64
|
+
const lines = [];
|
|
65
|
+
for (const s of statuses) {
|
|
66
|
+
const icon = s.blocked ? "🔴" : s.inputPercent > 80 || s.outputPercent > 80 ? "🟡" : "🟢";
|
|
67
|
+
lines.push(` ${icon} ${s.model}: ${s.inputTokensUsed.toLocaleString()}/${s.inputLimit.toLocaleString()} in, ${s.outputTokensUsed.toLocaleString()}/${s.outputLimit.toLocaleString()} out (${Math.round(Math.max(s.inputPercent, s.outputPercent))}%)`);
|
|
68
|
+
}
|
|
69
|
+
return lines;
|
|
70
|
+
}
|
|
71
|
+
prune(now) {
|
|
72
|
+
const cutoff = now - this.windowMs * 2;
|
|
73
|
+
this.samples = this.samples.filter((s) => s.timestamp >= cutoff);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=token-quota.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { WorkflowState } from "./workflow-engine.js";
|
|
2
|
+
export interface WorkflowChainEntry {
|
|
3
|
+
workflowName: string;
|
|
4
|
+
dependsOn: string[];
|
|
5
|
+
status: "pending" | "active" | "completed" | "failed";
|
|
6
|
+
}
|
|
7
|
+
export interface WorkflowChain {
|
|
8
|
+
name: string;
|
|
9
|
+
entries: WorkflowChainEntry[];
|
|
10
|
+
startedAt: number;
|
|
11
|
+
completedAt?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Create a workflow chain from a list of entries.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createWorkflowChain(name: string, entries: Array<{
|
|
17
|
+
workflowName: string;
|
|
18
|
+
dependsOn?: string[];
|
|
19
|
+
}>, now?: number): WorkflowChain;
|
|
20
|
+
/**
|
|
21
|
+
* Advance the chain: activate workflows whose dependencies are met.
|
|
22
|
+
* Returns workflow names that should be started.
|
|
23
|
+
*/
|
|
24
|
+
export declare function advanceChain(chain: WorkflowChain, workflowStates: ReadonlyMap<string, WorkflowState>): {
|
|
25
|
+
activate: string[];
|
|
26
|
+
completed: boolean;
|
|
27
|
+
failed: boolean;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Format workflow chain for TUI display.
|
|
31
|
+
*/
|
|
32
|
+
export declare function formatWorkflowChain(chain: WorkflowChain): string[];
|
|
33
|
+
//# sourceMappingURL=workflow-chain.d.ts.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// workflow-chain.ts — chain workflows together with dependencies.
|
|
2
|
+
// a workflow chain is a sequence of workflows where each can depend on
|
|
3
|
+
// the completion of previous workflows before starting.
|
|
4
|
+
/**
|
|
5
|
+
* Create a workflow chain from a list of entries.
|
|
6
|
+
*/
|
|
7
|
+
export function createWorkflowChain(name, entries, now = Date.now()) {
|
|
8
|
+
return {
|
|
9
|
+
name,
|
|
10
|
+
entries: entries.map((e) => ({
|
|
11
|
+
workflowName: e.workflowName,
|
|
12
|
+
dependsOn: e.dependsOn ?? [],
|
|
13
|
+
status: "pending",
|
|
14
|
+
})),
|
|
15
|
+
startedAt: now,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Advance the chain: activate workflows whose dependencies are met.
|
|
20
|
+
* Returns workflow names that should be started.
|
|
21
|
+
*/
|
|
22
|
+
export function advanceChain(chain, workflowStates) {
|
|
23
|
+
const completedSet = new Set();
|
|
24
|
+
const failedSet = new Set();
|
|
25
|
+
for (const entry of chain.entries) {
|
|
26
|
+
const wfState = workflowStates.get(entry.workflowName);
|
|
27
|
+
if (entry.status === "completed" || wfState?.completedAt) {
|
|
28
|
+
entry.status = "completed";
|
|
29
|
+
completedSet.add(entry.workflowName);
|
|
30
|
+
}
|
|
31
|
+
if (entry.status === "failed") {
|
|
32
|
+
failedSet.add(entry.workflowName);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// check for failed chains
|
|
36
|
+
if (failedSet.size > 0) {
|
|
37
|
+
return { activate: [], completed: false, failed: true };
|
|
38
|
+
}
|
|
39
|
+
const activate = [];
|
|
40
|
+
for (const entry of chain.entries) {
|
|
41
|
+
if (entry.status !== "pending")
|
|
42
|
+
continue;
|
|
43
|
+
const depsmet = entry.dependsOn.every((dep) => completedSet.has(dep));
|
|
44
|
+
if (depsmet) {
|
|
45
|
+
entry.status = "active";
|
|
46
|
+
activate.push(entry.workflowName);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const allDone = chain.entries.every((e) => e.status === "completed");
|
|
50
|
+
if (allDone)
|
|
51
|
+
chain.completedAt = Date.now();
|
|
52
|
+
return { activate, completed: allDone, failed: false };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Format workflow chain for TUI display.
|
|
56
|
+
*/
|
|
57
|
+
export function formatWorkflowChain(chain) {
|
|
58
|
+
const lines = [];
|
|
59
|
+
const elapsed = chain.completedAt ? chain.completedAt - chain.startedAt : Date.now() - chain.startedAt;
|
|
60
|
+
const dur = elapsed < 60_000 ? `${Math.round(elapsed / 1000)}s` : `${Math.round(elapsed / 60_000)}m`;
|
|
61
|
+
lines.push(` Workflow chain: ${chain.name} (${dur})`);
|
|
62
|
+
for (const e of chain.entries) {
|
|
63
|
+
const icon = e.status === "completed" ? "✓" : e.status === "active" ? "▶" : e.status === "failed" ? "✗" : "○";
|
|
64
|
+
const deps = e.dependsOn.length > 0 ? ` ← [${e.dependsOn.join(", ")}]` : "";
|
|
65
|
+
lines.push(` ${icon} ${e.workflowName} (${e.status})${deps}`);
|
|
66
|
+
}
|
|
67
|
+
return lines;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=workflow-chain.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { WorkflowDefinition } from "./workflow-engine.js";
|
|
2
|
+
export interface WorkflowCostForecast {
|
|
3
|
+
workflowName: string;
|
|
4
|
+
totalEstimatedCostUsd: number;
|
|
5
|
+
totalEstimatedHours: number;
|
|
6
|
+
stages: Array<{
|
|
7
|
+
name: string;
|
|
8
|
+
taskCount: number;
|
|
9
|
+
estimatedCostUsd: number;
|
|
10
|
+
estimatedHours: number;
|
|
11
|
+
}>;
|
|
12
|
+
confidence: "low" | "medium" | "high";
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Estimate the total cost of running a workflow.
|
|
16
|
+
*/
|
|
17
|
+
export declare function forecastWorkflowCost(workflow: WorkflowDefinition, historicalCostPerHour?: number): WorkflowCostForecast;
|
|
18
|
+
/**
|
|
19
|
+
* Format workflow cost forecast for TUI display.
|
|
20
|
+
*/
|
|
21
|
+
export declare function formatWorkflowCostForecast(forecast: WorkflowCostForecast): string[];
|
|
22
|
+
//# sourceMappingURL=workflow-cost-forecast.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// workflow-cost-forecast.ts — estimate total workflow cost before starting.
|
|
2
|
+
// uses per-stage task difficulty + historical cost patterns to project spend.
|
|
3
|
+
import { scoreDifficulty } from "./difficulty-scorer.js";
|
|
4
|
+
// rough cost per difficulty point per hour (based on typical LLM usage)
|
|
5
|
+
const COST_PER_DIFFICULTY_HOUR = 0.50; // $0.50/difficulty-point/hour
|
|
6
|
+
/**
|
|
7
|
+
* Estimate the total cost of running a workflow.
|
|
8
|
+
*/
|
|
9
|
+
export function forecastWorkflowCost(workflow, historicalCostPerHour) {
|
|
10
|
+
const costRate = historicalCostPerHour ?? COST_PER_DIFFICULTY_HOUR;
|
|
11
|
+
let totalCost = 0;
|
|
12
|
+
let totalHours = 0;
|
|
13
|
+
const stages = workflow.stages.map((stage) => {
|
|
14
|
+
let stageCost = 0;
|
|
15
|
+
let stageHours = 0;
|
|
16
|
+
for (const task of stage.tasks) {
|
|
17
|
+
const difficulty = scoreDifficulty(task.sessionTitle, task.goal);
|
|
18
|
+
const hours = difficulty.estimatedHours;
|
|
19
|
+
const cost = hours * costRate * (difficulty.score / 5); // scale by difficulty
|
|
20
|
+
stageCost += cost;
|
|
21
|
+
stageHours = Math.max(stageHours, hours); // parallel tasks: max duration
|
|
22
|
+
}
|
|
23
|
+
totalCost += stageCost;
|
|
24
|
+
totalHours += stageHours; // sequential stages: sum durations
|
|
25
|
+
return {
|
|
26
|
+
name: stage.name,
|
|
27
|
+
taskCount: stage.tasks.length,
|
|
28
|
+
estimatedCostUsd: Math.round(stageCost * 100) / 100,
|
|
29
|
+
estimatedHours: Math.round(stageHours * 10) / 10,
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
const confidence = historicalCostPerHour ? "medium" : "low";
|
|
33
|
+
return {
|
|
34
|
+
workflowName: workflow.name,
|
|
35
|
+
totalEstimatedCostUsd: Math.round(totalCost * 100) / 100,
|
|
36
|
+
totalEstimatedHours: Math.round(totalHours * 10) / 10,
|
|
37
|
+
stages,
|
|
38
|
+
confidence,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Format workflow cost forecast for TUI display.
|
|
43
|
+
*/
|
|
44
|
+
export function formatWorkflowCostForecast(forecast) {
|
|
45
|
+
const conf = forecast.confidence === "high" ? "●" : forecast.confidence === "medium" ? "◐" : "○";
|
|
46
|
+
const lines = [];
|
|
47
|
+
lines.push(` ${conf} Workflow cost forecast: "${forecast.workflowName}"`);
|
|
48
|
+
lines.push(` Total: ~$${forecast.totalEstimatedCostUsd.toFixed(2)} over ~${forecast.totalEstimatedHours}h`);
|
|
49
|
+
lines.push("");
|
|
50
|
+
for (const s of forecast.stages) {
|
|
51
|
+
lines.push(` ${s.name}: ${s.taskCount} task${s.taskCount !== 1 ? "s" : ""}, ~$${s.estimatedCostUsd.toFixed(2)}, ~${s.estimatedHours}h`);
|
|
52
|
+
}
|
|
53
|
+
return lines;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=workflow-cost-forecast.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface WorkflowStage {
|
|
2
|
+
name: string;
|
|
3
|
+
tasks: Array<{
|
|
4
|
+
sessionTitle: string;
|
|
5
|
+
goal: string;
|
|
6
|
+
}>;
|
|
7
|
+
dependsOnStage?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface WorkflowDefinition {
|
|
10
|
+
name: string;
|
|
11
|
+
stages: WorkflowStage[];
|
|
12
|
+
}
|
|
13
|
+
export interface WorkflowState {
|
|
14
|
+
name: string;
|
|
15
|
+
currentStage: number;
|
|
16
|
+
stages: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
status: "pending" | "active" | "completed" | "failed";
|
|
19
|
+
tasks: Array<{
|
|
20
|
+
sessionTitle: string;
|
|
21
|
+
status: string;
|
|
22
|
+
}>;
|
|
23
|
+
}>;
|
|
24
|
+
startedAt: number;
|
|
25
|
+
completedAt?: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Create a workflow state from a definition.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createWorkflowState(def: WorkflowDefinition, now?: number): WorkflowState;
|
|
31
|
+
/**
|
|
32
|
+
* Advance the workflow based on current task states.
|
|
33
|
+
* Returns actions to take (activate next stage's tasks, complete workflow, etc.)
|
|
34
|
+
*/
|
|
35
|
+
export declare function advanceWorkflow(workflow: WorkflowState, taskStates: ReadonlyMap<string, string>): {
|
|
36
|
+
actions: WorkflowAction[];
|
|
37
|
+
completed: boolean;
|
|
38
|
+
};
|
|
39
|
+
export interface WorkflowAction {
|
|
40
|
+
type: "activate_task" | "stage_started" | "stage_completed" | "stage_failed" | "workflow_completed";
|
|
41
|
+
stage: string;
|
|
42
|
+
detail: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Format workflow state for TUI display.
|
|
46
|
+
*/
|
|
47
|
+
export declare function formatWorkflow(workflow: WorkflowState): string[];
|
|
48
|
+
//# sourceMappingURL=workflow-engine.d.ts.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// workflow-engine.ts — define multi-session workflows with fan-out/fan-in.
|
|
2
|
+
// a workflow is a DAG of stages, each containing one or more parallel tasks.
|
|
3
|
+
// stages execute sequentially; tasks within a stage execute in parallel.
|
|
4
|
+
/**
|
|
5
|
+
* Create a workflow state from a definition.
|
|
6
|
+
*/
|
|
7
|
+
export function createWorkflowState(def, now = Date.now()) {
|
|
8
|
+
return {
|
|
9
|
+
name: def.name,
|
|
10
|
+
currentStage: 0,
|
|
11
|
+
stages: def.stages.map((s) => ({
|
|
12
|
+
name: s.name,
|
|
13
|
+
status: "pending",
|
|
14
|
+
tasks: s.tasks.map((t) => ({ sessionTitle: t.sessionTitle, status: "pending" })),
|
|
15
|
+
})),
|
|
16
|
+
startedAt: now,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Advance the workflow based on current task states.
|
|
21
|
+
* Returns actions to take (activate next stage's tasks, complete workflow, etc.)
|
|
22
|
+
*/
|
|
23
|
+
export function advanceWorkflow(workflow, taskStates) {
|
|
24
|
+
const actions = [];
|
|
25
|
+
if (workflow.currentStage >= workflow.stages.length) {
|
|
26
|
+
return { actions: [], completed: true };
|
|
27
|
+
}
|
|
28
|
+
const current = workflow.stages[workflow.currentStage];
|
|
29
|
+
// update task statuses from live data
|
|
30
|
+
for (const task of current.tasks) {
|
|
31
|
+
const liveStatus = taskStates.get(task.sessionTitle);
|
|
32
|
+
if (liveStatus)
|
|
33
|
+
task.status = liveStatus;
|
|
34
|
+
}
|
|
35
|
+
// check if current stage is completed
|
|
36
|
+
const allDone = current.tasks.every((t) => t.status === "completed");
|
|
37
|
+
const anyFailed = current.tasks.some((t) => t.status === "failed");
|
|
38
|
+
if (anyFailed) {
|
|
39
|
+
current.status = "failed";
|
|
40
|
+
actions.push({ type: "stage_failed", stage: current.name, detail: `stage "${current.name}" has failed tasks` });
|
|
41
|
+
return { actions, completed: false };
|
|
42
|
+
}
|
|
43
|
+
if (allDone) {
|
|
44
|
+
current.status = "completed";
|
|
45
|
+
workflow.currentStage++;
|
|
46
|
+
if (workflow.currentStage >= workflow.stages.length) {
|
|
47
|
+
workflow.completedAt = Date.now();
|
|
48
|
+
actions.push({ type: "workflow_completed", stage: workflow.name, detail: `workflow "${workflow.name}" completed` });
|
|
49
|
+
return { actions, completed: true };
|
|
50
|
+
}
|
|
51
|
+
// activate next stage
|
|
52
|
+
const next = workflow.stages[workflow.currentStage];
|
|
53
|
+
next.status = "active";
|
|
54
|
+
for (const task of next.tasks) {
|
|
55
|
+
actions.push({ type: "activate_task", stage: next.name, detail: task.sessionTitle });
|
|
56
|
+
}
|
|
57
|
+
actions.push({ type: "stage_started", stage: next.name, detail: `stage "${next.name}" started (${next.tasks.length} tasks)` });
|
|
58
|
+
}
|
|
59
|
+
else if (current.status === "pending") {
|
|
60
|
+
// activate first stage
|
|
61
|
+
current.status = "active";
|
|
62
|
+
for (const task of current.tasks) {
|
|
63
|
+
actions.push({ type: "activate_task", stage: current.name, detail: task.sessionTitle });
|
|
64
|
+
}
|
|
65
|
+
actions.push({ type: "stage_started", stage: current.name, detail: `stage "${current.name}" started (${current.tasks.length} tasks)` });
|
|
66
|
+
}
|
|
67
|
+
return { actions, completed: false };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Format workflow state for TUI display.
|
|
71
|
+
*/
|
|
72
|
+
export function formatWorkflow(workflow) {
|
|
73
|
+
const lines = [];
|
|
74
|
+
const elapsed = workflow.completedAt
|
|
75
|
+
? workflow.completedAt - workflow.startedAt
|
|
76
|
+
: Date.now() - workflow.startedAt;
|
|
77
|
+
const duration = elapsed < 60_000 ? `${Math.round(elapsed / 1000)}s` : `${Math.round(elapsed / 60_000)}m`;
|
|
78
|
+
lines.push(` Workflow: ${workflow.name} (${duration})`);
|
|
79
|
+
for (let i = 0; i < workflow.stages.length; i++) {
|
|
80
|
+
const s = workflow.stages[i];
|
|
81
|
+
const icon = s.status === "completed" ? "✓" : s.status === "active" ? "▶" : s.status === "failed" ? "✗" : "○";
|
|
82
|
+
const current = i === workflow.currentStage ? " ←" : "";
|
|
83
|
+
lines.push(` ${icon} Stage ${i + 1}: ${s.name} (${s.status})${current}`);
|
|
84
|
+
for (const t of s.tasks) {
|
|
85
|
+
const tIcon = t.status === "completed" ? "✓" : t.status === "active" ? "~" : t.status === "failed" ? "!" : ".";
|
|
86
|
+
lines.push(` [${tIcon}] ${t.sessionTitle}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return lines;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=workflow-engine.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { WorkflowDefinition } from "./workflow-engine.js";
|
|
2
|
+
export interface WorkflowTemplate {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
stages: Array<{
|
|
6
|
+
name: string;
|
|
7
|
+
taskGoals: string[];
|
|
8
|
+
}>;
|
|
9
|
+
tags: string[];
|
|
10
|
+
}
|
|
11
|
+
export declare const BUILTIN_WORKFLOW_TEMPLATES: WorkflowTemplate[];
|
|
12
|
+
/**
|
|
13
|
+
* Find a workflow template by name.
|
|
14
|
+
*/
|
|
15
|
+
export declare function findWorkflowTemplate(name: string): WorkflowTemplate | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Instantiate a workflow definition from a template.
|
|
18
|
+
* sessionPrefix is used to generate session titles for each task.
|
|
19
|
+
*/
|
|
20
|
+
export declare function instantiateWorkflow(template: WorkflowTemplate, sessionPrefix: string): WorkflowDefinition;
|
|
21
|
+
/**
|
|
22
|
+
* Format workflow template list for TUI display.
|
|
23
|
+
*/
|
|
24
|
+
export declare function formatWorkflowTemplateList(): string[];
|
|
25
|
+
//# sourceMappingURL=workflow-templates.d.ts.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// workflow-templates.ts — pre-built workflow definitions for common patterns.
|
|
2
|
+
// each template produces a WorkflowDefinition ready to instantiate.
|
|
3
|
+
export const BUILTIN_WORKFLOW_TEMPLATES = [
|
|
4
|
+
{
|
|
5
|
+
name: "ci-cd",
|
|
6
|
+
description: "Build → Test → Deploy pipeline",
|
|
7
|
+
stages: [
|
|
8
|
+
{ name: "build", taskGoals: ["Build the project and fix any compilation errors"] },
|
|
9
|
+
{ name: "test", taskGoals: ["Run the full test suite and fix any failures"] },
|
|
10
|
+
{ name: "deploy", taskGoals: ["Deploy to staging/production and verify health checks"] },
|
|
11
|
+
],
|
|
12
|
+
tags: ["ci", "cd", "deploy"],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "feature-dev",
|
|
16
|
+
description: "Implement → Test → Review → Merge",
|
|
17
|
+
stages: [
|
|
18
|
+
{ name: "implement", taskGoals: ["Implement the feature according to the spec"] },
|
|
19
|
+
{ name: "test", taskGoals: ["Write unit and integration tests for the new feature", "Run existing tests to verify no regressions"] },
|
|
20
|
+
{ name: "review", taskGoals: ["Self-review: check for code quality, security, and documentation"] },
|
|
21
|
+
{ name: "merge", taskGoals: ["Create PR, address feedback, merge to main"] },
|
|
22
|
+
],
|
|
23
|
+
tags: ["feature", "development"],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "refactor",
|
|
27
|
+
description: "Analyze → Refactor → Test → Cleanup",
|
|
28
|
+
stages: [
|
|
29
|
+
{ name: "analyze", taskGoals: ["Identify code smells, duplications, and improvement opportunities"] },
|
|
30
|
+
{ name: "refactor", taskGoals: ["Apply refactoring changes incrementally with tests passing at each step"] },
|
|
31
|
+
{ name: "test", taskGoals: ["Run full test suite, fix any regressions from refactoring"] },
|
|
32
|
+
{ name: "cleanup", taskGoals: ["Remove dead code, update documentation, commit with clear message"] },
|
|
33
|
+
],
|
|
34
|
+
tags: ["refactor", "cleanup"],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "incident-response",
|
|
38
|
+
description: "Triage → Fix → Test → Postmortem",
|
|
39
|
+
stages: [
|
|
40
|
+
{ name: "triage", taskGoals: ["Identify root cause from logs and error output"] },
|
|
41
|
+
{ name: "fix", taskGoals: ["Implement the fix with minimal blast radius"] },
|
|
42
|
+
{ name: "test", taskGoals: ["Verify fix resolves the issue, run regression tests"] },
|
|
43
|
+
{ name: "postmortem", taskGoals: ["Document what happened, why, and how to prevent recurrence"] },
|
|
44
|
+
],
|
|
45
|
+
tags: ["incident", "hotfix", "production"],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "multi-repo",
|
|
49
|
+
description: "Parallel work across multiple repositories",
|
|
50
|
+
stages: [
|
|
51
|
+
{ name: "parallel-work", taskGoals: ["Complete assigned work in each repository"] },
|
|
52
|
+
{ name: "integration", taskGoals: ["Verify cross-repo integration works correctly"] },
|
|
53
|
+
{ name: "release", taskGoals: ["Tag releases and update dependency references"] },
|
|
54
|
+
],
|
|
55
|
+
tags: ["multi-repo", "monorepo"],
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
59
|
+
* Find a workflow template by name.
|
|
60
|
+
*/
|
|
61
|
+
export function findWorkflowTemplate(name) {
|
|
62
|
+
return BUILTIN_WORKFLOW_TEMPLATES.find((t) => t.name.toLowerCase() === name.toLowerCase());
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Instantiate a workflow definition from a template.
|
|
66
|
+
* sessionPrefix is used to generate session titles for each task.
|
|
67
|
+
*/
|
|
68
|
+
export function instantiateWorkflow(template, sessionPrefix) {
|
|
69
|
+
let taskCounter = 0;
|
|
70
|
+
const stages = template.stages.map((s) => ({
|
|
71
|
+
name: s.name,
|
|
72
|
+
tasks: s.taskGoals.map((goal) => {
|
|
73
|
+
taskCounter++;
|
|
74
|
+
return { sessionTitle: `${sessionPrefix}-${s.name}-${taskCounter}`, goal };
|
|
75
|
+
}),
|
|
76
|
+
}));
|
|
77
|
+
return { name: `${sessionPrefix}-${template.name}`, stages };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Format workflow template list for TUI display.
|
|
81
|
+
*/
|
|
82
|
+
export function formatWorkflowTemplateList() {
|
|
83
|
+
const lines = [];
|
|
84
|
+
lines.push(` Workflow templates (${BUILTIN_WORKFLOW_TEMPLATES.length} built-in):`);
|
|
85
|
+
for (const t of BUILTIN_WORKFLOW_TEMPLATES) {
|
|
86
|
+
const stageNames = t.stages.map((s) => s.name).join(" → ");
|
|
87
|
+
lines.push(` ${t.name.padEnd(18)} ${stageNames} [${t.tags.join(", ")}]`);
|
|
88
|
+
lines.push(` ${t.description}`);
|
|
89
|
+
}
|
|
90
|
+
return lines;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=workflow-templates.js.map
|