@kodrunhq/opencode-autopilot 1.17.0 → 1.19.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.
Files changed (118) hide show
  1. package/README.md +95 -13
  2. package/assets/commands/oc-doctor.md +17 -0
  3. package/assets/commands/oc-update-docs.md +1 -1
  4. package/bin/configure-tui.ts +1 -1
  5. package/package.json +1 -1
  6. package/src/agents/index.ts +0 -12
  7. package/src/agents/pipeline/index.ts +0 -4
  8. package/src/autonomy/completion.ts +52 -0
  9. package/src/autonomy/controller.ts +144 -0
  10. package/src/autonomy/index.ts +25 -0
  11. package/src/autonomy/injector.ts +49 -0
  12. package/src/autonomy/state.ts +91 -0
  13. package/src/autonomy/types.ts +30 -0
  14. package/src/autonomy/verification.ts +86 -0
  15. package/src/background/database.ts +170 -0
  16. package/src/background/executor.ts +174 -0
  17. package/src/background/index.ts +8 -0
  18. package/src/background/manager.ts +232 -0
  19. package/src/background/repository.ts +174 -0
  20. package/src/background/schema.ts +24 -0
  21. package/src/background/sdk-runner.ts +40 -0
  22. package/src/background/slot-manager.ts +41 -0
  23. package/src/background/state-machine.ts +19 -0
  24. package/src/config/v7.ts +3 -3
  25. package/src/config.ts +105 -21
  26. package/src/context/budget.ts +45 -0
  27. package/src/context/compaction-handler.ts +58 -0
  28. package/src/context/discovery.ts +94 -0
  29. package/src/context/index.ts +14 -0
  30. package/src/context/injector.ts +119 -0
  31. package/src/context/types.ts +24 -0
  32. package/src/health/checks.ts +214 -3
  33. package/src/health/index.ts +7 -1
  34. package/src/health/runner.ts +14 -2
  35. package/src/index.ts +113 -6
  36. package/src/installer.ts +13 -0
  37. package/src/kernel/index.ts +6 -0
  38. package/src/kernel/migrations.ts +50 -0
  39. package/src/kernel/retry.ts +49 -0
  40. package/src/kernel/schema.ts +9 -1
  41. package/src/kernel/transaction.ts +40 -12
  42. package/src/logging/forensic-writer.ts +6 -2
  43. package/src/logging/index.ts +2 -0
  44. package/src/mcp/index.ts +34 -0
  45. package/src/mcp/manager.ts +206 -0
  46. package/src/mcp/scope-filter.ts +44 -0
  47. package/src/mcp/types.ts +38 -0
  48. package/src/orchestrator/arena.ts +7 -1
  49. package/src/orchestrator/fallback/event-handler.ts +12 -1
  50. package/src/orchestrator/handlers/challenge.ts +8 -1
  51. package/src/orchestrator/handlers/plan.ts +8 -1
  52. package/src/orchestrator/handlers/recon.ts +8 -1
  53. package/src/orchestrator/handlers/types.ts +2 -2
  54. package/src/orchestrator/lesson-memory.ts +6 -1
  55. package/src/orchestrator/orchestration-logger.ts +15 -3
  56. package/src/orchestrator/skill-injection.ts +7 -1
  57. package/src/orchestrator/state.ts +6 -1
  58. package/src/recovery/classifier.ts +127 -0
  59. package/src/recovery/event-handler.ts +263 -0
  60. package/src/recovery/index.ts +20 -0
  61. package/src/recovery/orchestrator.ts +180 -0
  62. package/src/recovery/persistence.ts +87 -0
  63. package/src/recovery/strategies.ts +107 -0
  64. package/src/recovery/types.ts +31 -0
  65. package/src/registry/model-groups.ts +2 -19
  66. package/src/registry/resolver.ts +38 -9
  67. package/src/review/agent-catalog.ts +83 -251
  68. package/src/review/agents/architecture-verifier.ts +41 -0
  69. package/src/review/agents/code-hygiene-auditor.ts +40 -0
  70. package/src/review/agents/correctness-auditor.ts +41 -0
  71. package/src/review/agents/frontend-auditor.ts +39 -0
  72. package/src/review/agents/index.ts +15 -42
  73. package/src/review/agents/language-idioms-auditor.ts +39 -0
  74. package/src/review/agents/security-auditor.ts +12 -8
  75. package/src/review/stack-gate.ts +2 -6
  76. package/src/routing/categories.ts +111 -0
  77. package/src/routing/classifier.ts +152 -0
  78. package/src/routing/engine.ts +89 -0
  79. package/src/routing/index.ts +4 -0
  80. package/src/routing/types.ts +14 -0
  81. package/src/skills/adaptive-injector.ts +34 -3
  82. package/src/skills/loader.ts +4 -0
  83. package/src/tools/background.ts +196 -0
  84. package/src/tools/configure.ts +1 -1
  85. package/src/tools/delegate.ts +205 -0
  86. package/src/tools/loop.ts +94 -0
  87. package/src/tools/recover.ts +172 -0
  88. package/src/types/background.ts +51 -0
  89. package/src/types/mcp.ts +27 -0
  90. package/src/types/recovery.ts +49 -0
  91. package/src/types/routing.ts +39 -0
  92. package/src/ux/context-warnings.ts +81 -0
  93. package/src/ux/error-hints.ts +38 -0
  94. package/src/ux/index.ts +7 -0
  95. package/src/ux/notifications.ts +67 -0
  96. package/src/ux/progress.ts +77 -0
  97. package/src/ux/session-summary.ts +67 -0
  98. package/src/ux/task-status.ts +109 -0
  99. package/src/ux/types.ts +24 -0
  100. package/src/agents/db-specialist.ts +0 -295
  101. package/src/agents/devops.ts +0 -352
  102. package/src/agents/documenter.ts +0 -44
  103. package/src/agents/frontend-engineer.ts +0 -541
  104. package/src/agents/pipeline/oc-explorer.ts +0 -46
  105. package/src/agents/pipeline/oc-retrospector.ts +0 -42
  106. package/src/review/agents/auth-flow-verifier.ts +0 -47
  107. package/src/review/agents/concurrency-checker.ts +0 -47
  108. package/src/review/agents/dead-code-scanner.ts +0 -47
  109. package/src/review/agents/go-idioms-auditor.ts +0 -46
  110. package/src/review/agents/python-django-auditor.ts +0 -46
  111. package/src/review/agents/react-patterns-auditor.ts +0 -46
  112. package/src/review/agents/rust-safety-auditor.ts +0 -46
  113. package/src/review/agents/scope-intent-verifier.ts +0 -45
  114. package/src/review/agents/silent-failure-hunter.ts +0 -45
  115. package/src/review/agents/spec-checker.ts +0 -45
  116. package/src/review/agents/state-mgmt-auditor.ts +0 -46
  117. package/src/review/agents/type-soundness.ts +0 -46
  118. package/src/review/agents/wiring-inspector.ts +0 -46
@@ -0,0 +1,51 @@
1
+ import { z } from "zod";
2
+
3
+ export const TaskStatusSchema = z.enum(["pending", "running", "completed", "failed", "cancelled"]);
4
+ export type TaskStatus = z.infer<typeof TaskStatusSchema>;
5
+
6
+ export const TaskResultSchema = z.object({
7
+ taskId: z.string().min(1),
8
+ status: TaskStatusSchema,
9
+ output: z.string().optional(),
10
+ error: z.string().optional(),
11
+ startedAt: z.number().optional(),
12
+ completedAt: z.number().optional(),
13
+ durationMs: z.number().optional(),
14
+ });
15
+ export type TaskResult = z.infer<typeof TaskResultSchema>;
16
+
17
+ export const BackgroundTaskSchema = z.object({
18
+ id: z.string().min(1),
19
+ name: z.string().min(1),
20
+ status: TaskStatusSchema,
21
+ agentId: z.string().optional(),
22
+ priority: z.number().int().min(0).max(100).default(50),
23
+ createdAt: z.number(),
24
+ result: TaskResultSchema.optional(),
25
+ metadata: z.record(z.string(), z.unknown()).default({}),
26
+ });
27
+ export type BackgroundTask = z.infer<typeof BackgroundTaskSchema>;
28
+
29
+ export const AgentSlotSchema = z.object({
30
+ slotId: z.string().min(1),
31
+ agentId: z.string().min(1),
32
+ capacity: z.number().int().min(1).max(100).default(1),
33
+ activeTaskCount: z.number().int().min(0).default(0),
34
+ reserved: z.boolean().default(false),
35
+ });
36
+ export type AgentSlot = z.infer<typeof AgentSlotSchema>;
37
+
38
+ export const ConcurrencyLimitsSchema = z.object({
39
+ global: z.number().int().min(1).max(50).default(5),
40
+ perAgent: z.number().int().min(1).max(10).default(2),
41
+ perCategory: z.record(z.string(), z.number().int().min(1).max(20)).default({}),
42
+ });
43
+ export type ConcurrencyLimits = z.infer<typeof ConcurrencyLimitsSchema>;
44
+
45
+ export const backgroundConfigSchema = z.object({
46
+ enabled: z.boolean().default(false),
47
+ maxConcurrent: z.number().int().min(1).max(50).default(5),
48
+ persistence: z.boolean().default(true),
49
+ });
50
+ export type BackgroundConfig = z.infer<typeof backgroundConfigSchema>;
51
+ export const backgroundDefaults = backgroundConfigSchema.parse({});
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+
3
+ export const McpSkillSchema = z.object({
4
+ name: z.string().min(1),
5
+ description: z.string().optional(),
6
+ enabled: z.boolean().default(true),
7
+ version: z.string().optional(),
8
+ config: z.record(z.string(), z.unknown()).default({}),
9
+ });
10
+ export type McpSkill = z.infer<typeof McpSkillSchema>;
11
+
12
+ export const McpServerSchema = z.object({
13
+ id: z.string().min(1),
14
+ url: z.string().url().optional(),
15
+ transport: z.enum(["stdio", "http", "sse"]).default("stdio"),
16
+ enabled: z.boolean().default(true),
17
+ skills: z.array(z.string()).default([]),
18
+ metadata: z.record(z.string(), z.unknown()).default({}),
19
+ });
20
+ export type McpServer = z.infer<typeof McpServerSchema>;
21
+
22
+ export const mcpConfigSchema = z.object({
23
+ enabled: z.boolean().default(false),
24
+ skills: z.record(z.string(), z.unknown()).default({}),
25
+ });
26
+ export type McpConfig = z.infer<typeof mcpConfigSchema>;
27
+ export const mcpDefaults = mcpConfigSchema.parse({});
@@ -0,0 +1,49 @@
1
+ import { z } from "zod";
2
+
3
+ export const ErrorCategorySchema = z.enum([
4
+ "rate_limit",
5
+ "auth_failure",
6
+ "quota_exceeded",
7
+ "service_unavailable",
8
+ "timeout",
9
+ "network",
10
+ "validation",
11
+ "empty_content",
12
+ "thinking_block_error",
13
+ "tool_result_overflow",
14
+ "context_window_exceeded",
15
+ "session_corruption",
16
+ "agent_loop_stuck",
17
+ "unknown",
18
+ ]);
19
+ export type ErrorCategory = z.infer<typeof ErrorCategorySchema>;
20
+
21
+ export const RecoveryStrategySchema = z.enum([
22
+ "retry",
23
+ "fallback_model",
24
+ "skip",
25
+ "abort",
26
+ "user_prompt",
27
+ "compact_and_retry",
28
+ "restart_session",
29
+ "reduce_context",
30
+ "skip_and_continue",
31
+ ]);
32
+ export type RecoveryStrategy = z.infer<typeof RecoveryStrategySchema>;
33
+
34
+ export const RecoveryActionSchema = z.object({
35
+ strategy: RecoveryStrategySchema,
36
+ errorCategory: ErrorCategorySchema,
37
+ maxAttempts: z.number().int().min(1).max(10).default(3),
38
+ backoffMs: z.number().int().min(0).default(1000),
39
+ fallbackAgentId: z.string().optional(),
40
+ metadata: z.record(z.string(), z.unknown()).default({}),
41
+ });
42
+ export type RecoveryAction = z.infer<typeof RecoveryActionSchema>;
43
+
44
+ export const recoveryConfigSchema = z.object({
45
+ enabled: z.boolean().default(true),
46
+ maxRetries: z.number().int().min(0).max(10).default(3),
47
+ });
48
+ export type RecoveryConfig = z.infer<typeof recoveryConfigSchema>;
49
+ export const recoveryDefaults = recoveryConfigSchema.parse({});
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+
3
+ export const CategorySchema = z.enum([
4
+ "quick",
5
+ "visual-engineering",
6
+ "ultrabrain",
7
+ "artistry",
8
+ "writing",
9
+ "unspecified-high",
10
+ "unspecified-low",
11
+ ]);
12
+ export type Category = z.infer<typeof CategorySchema>;
13
+
14
+ export const CategoryConfigSchema = z.object({
15
+ enabled: z.boolean().default(true),
16
+ agentId: z.string().optional(),
17
+ modelGroup: z.string().optional(),
18
+ maxTokenBudget: z.number().int().min(1000).optional(),
19
+ timeoutSeconds: z.number().int().min(1).max(3600).optional(),
20
+ skills: z.array(z.string()).default([]),
21
+ metadata: z.record(z.string(), z.unknown()).default({}),
22
+ });
23
+ export type CategoryConfig = z.infer<typeof CategoryConfigSchema>;
24
+
25
+ export const RoutingDecisionSchema = z.object({
26
+ category: CategorySchema,
27
+ confidence: z.number().min(0).max(1),
28
+ agentId: z.string().optional(),
29
+ reasoning: z.string().optional(),
30
+ appliedConfig: CategoryConfigSchema.optional(),
31
+ });
32
+ export type RoutingDecision = z.infer<typeof RoutingDecisionSchema>;
33
+
34
+ export const routingConfigSchema = z.object({
35
+ enabled: z.boolean().default(false),
36
+ categories: z.record(z.string(), CategoryConfigSchema).default({}),
37
+ });
38
+ export type RoutingConfig = z.infer<typeof routingConfigSchema>;
39
+ export const routingDefaults = routingConfigSchema.parse({});
@@ -0,0 +1,81 @@
1
+ import type { NotificationManager } from "./notifications";
2
+
3
+ export type ContextWarningLevel = "none" | "moderate" | "high" | "critical";
4
+
5
+ export interface ContextWarningMonitorOptions {
6
+ readonly notificationManager?: NotificationManager;
7
+ }
8
+
9
+ const warnedLevelsBySeverity = Object.freeze({
10
+ moderate: Object.freeze({
11
+ title: "Context warning",
12
+ message: "Context at 70%, consider wrapping up",
13
+ notify: (manager: NotificationManager) =>
14
+ manager.info("Context warning", "Context at 70%, consider wrapping up"),
15
+ }),
16
+ high: Object.freeze({
17
+ title: "Context warning",
18
+ message: "Context at 85%, compaction recommended",
19
+ notify: (manager: NotificationManager) =>
20
+ manager.warn("Context warning", "Context at 85%, compaction recommended"),
21
+ }),
22
+ critical: Object.freeze({
23
+ title: "Context warning",
24
+ message: "Context at 95%, force compaction",
25
+ notify: (manager: NotificationManager) =>
26
+ manager.error("Context warning", "Context at 95%, force compaction"),
27
+ }),
28
+ });
29
+
30
+ export class ContextWarningMonitor {
31
+ private readonly notificationManager?: NotificationManager;
32
+ private readonly warnedLevels = new Set<Exclude<ContextWarningLevel, "none">>();
33
+ private warningLevel: ContextWarningLevel = "none";
34
+
35
+ constructor(options: ContextWarningMonitorOptions = {}) {
36
+ this.notificationManager = options.notificationManager;
37
+ }
38
+
39
+ checkUtilization(inputTokens: number, contextLimit: number): void {
40
+ if (contextLimit <= 0) {
41
+ this.warningLevel = "none";
42
+ return;
43
+ }
44
+
45
+ const utilization = inputTokens / contextLimit;
46
+ const nextLevel = getWarningLevelForUtilization(utilization);
47
+ this.warningLevel = nextLevel;
48
+
49
+ if (nextLevel === "none" || this.warnedLevels.has(nextLevel)) {
50
+ return;
51
+ }
52
+
53
+ this.warnedLevels.add(nextLevel);
54
+ const notifier = warnedLevelsBySeverity[nextLevel];
55
+ if (!notifier || !this.notificationManager) {
56
+ return;
57
+ }
58
+
59
+ void notifier.notify(this.notificationManager);
60
+ }
61
+
62
+ getWarningLevel(): ContextWarningLevel {
63
+ return this.warningLevel;
64
+ }
65
+ }
66
+
67
+ function getWarningLevelForUtilization(utilization: number): ContextWarningLevel {
68
+ if (utilization >= 0.95) {
69
+ return "critical";
70
+ }
71
+
72
+ if (utilization >= 0.85) {
73
+ return "high";
74
+ }
75
+
76
+ if (utilization >= 0.7) {
77
+ return "moderate";
78
+ }
79
+
80
+ return "none";
81
+ }
@@ -0,0 +1,38 @@
1
+ const REMEDIATION_HINTS = Object.freeze([
2
+ {
3
+ pattern: /429|rate limit/i,
4
+ hint: "Rate limited. Will retry in 30s.",
5
+ },
6
+ {
7
+ pattern: /context window|token limit/i,
8
+ hint: "Context window exceeded. Try compacting conversation.",
9
+ },
10
+ {
11
+ pattern: /auth|unauthorized/i,
12
+ hint: "Authentication failed. Check your API key.",
13
+ },
14
+ {
15
+ pattern: /timeout/i,
16
+ hint: "Request timed out. Will retry with longer timeout.",
17
+ },
18
+ {
19
+ pattern: /empty content/i,
20
+ hint: "Model returned empty response. Retrying with different model.",
21
+ },
22
+ {
23
+ pattern: /SQLITE_BUSY/i,
24
+ hint: "Database busy. Retrying with backoff.",
25
+ },
26
+ ]);
27
+
28
+ export function getRemediationHint(error: Error | string): string | null {
29
+ const message = typeof error === "string" ? error : error.message;
30
+
31
+ for (const entry of REMEDIATION_HINTS) {
32
+ if (entry.pattern.test(message)) {
33
+ return entry.hint;
34
+ }
35
+ }
36
+
37
+ return null;
38
+ }
@@ -0,0 +1,7 @@
1
+ export * from "./context-warnings";
2
+ export * from "./error-hints";
3
+ export * from "./notifications";
4
+ export * from "./progress";
5
+ export * from "./session-summary";
6
+ export * from "./task-status";
7
+ export * from "./types";
@@ -0,0 +1,67 @@
1
+ import { getLogger } from "../logging/domains";
2
+ import type { NotificationSink, ToastOptions, ToastVariant } from "./types";
3
+
4
+ const DEFAULT_RATE_LIMIT_MS = 5_000;
5
+
6
+ const logger = getLogger("ux", "notifications");
7
+
8
+ export interface NotificationManagerOptions {
9
+ readonly sink?: NotificationSink;
10
+ readonly rateLimitMs?: number;
11
+ }
12
+
13
+ export class NotificationManager {
14
+ private readonly sink?: NotificationSink;
15
+ private readonly rateLimitMs: number;
16
+ private readonly lastToastAtByVariant = new Map<ToastVariant, number>();
17
+
18
+ constructor(options: NotificationManagerOptions = {}) {
19
+ this.sink = options.sink;
20
+ this.rateLimitMs = options.rateLimitMs ?? DEFAULT_RATE_LIMIT_MS;
21
+ }
22
+
23
+ async notify(options: ToastOptions): Promise<void> {
24
+ try {
25
+ if (!this.sink) {
26
+ return;
27
+ }
28
+
29
+ const now = Date.now();
30
+ const lastToastAt = this.lastToastAtByVariant.get(options.variant) ?? 0;
31
+
32
+ if (now - lastToastAt < this.rateLimitMs) {
33
+ logger.debug("Skipped rate-limited toast", {
34
+ variant: options.variant,
35
+ title: options.title,
36
+ rateLimitMs: this.rateLimitMs,
37
+ });
38
+ return;
39
+ }
40
+
41
+ await this.sink.showToast(options.title, options.message, options.variant, options.duration);
42
+ this.lastToastAtByVariant.set(options.variant, now);
43
+ } catch (error: unknown) {
44
+ logger.debug("Toast delivery failed", {
45
+ variant: options.variant,
46
+ title: options.title,
47
+ error: String(error),
48
+ });
49
+ }
50
+ }
51
+
52
+ info(title: string, message: string, duration?: number): Promise<void> {
53
+ return this.notify({ title, message, variant: "info", duration });
54
+ }
55
+
56
+ success(title: string, message: string, duration?: number): Promise<void> {
57
+ return this.notify({ title, message, variant: "success", duration });
58
+ }
59
+
60
+ warn(title: string, message: string, duration?: number): Promise<void> {
61
+ return this.notify({ title, message, variant: "warning", duration });
62
+ }
63
+
64
+ error(title: string, message: string, duration?: number): Promise<void> {
65
+ return this.notify({ title, message, variant: "error", duration });
66
+ }
67
+ }
@@ -0,0 +1,77 @@
1
+ import type { NotificationManager } from "./notifications";
2
+ import type { ProgressUpdate } from "./types";
3
+
4
+ export interface ProgressTrackerOptions {
5
+ readonly notificationManager?: NotificationManager;
6
+ }
7
+
8
+ export class ProgressTracker {
9
+ private readonly notificationManager?: NotificationManager;
10
+ private progress: ProgressUpdate | null = null;
11
+ private phaseName: string | null = null;
12
+
13
+ constructor(options: ProgressTrackerOptions = {}) {
14
+ this.notificationManager = options.notificationManager;
15
+ }
16
+
17
+ startPhase(phaseName: string, totalSteps: number): void {
18
+ const normalizedTotal = Math.max(0, totalSteps);
19
+ this.phaseName = phaseName;
20
+ this.progress = Object.freeze({
21
+ current: 0,
22
+ total: normalizedTotal,
23
+ label: phaseName,
24
+ detail: "Starting phase",
25
+ });
26
+
27
+ void this.notificationManager?.info("Phase started", `${phaseName} has started.`);
28
+ }
29
+
30
+ advanceStep(label: string): void {
31
+ if (!this.progress) {
32
+ return;
33
+ }
34
+
35
+ const nextCurrent =
36
+ this.progress.total > 0
37
+ ? Math.min(this.progress.current + 1, this.progress.total)
38
+ : this.progress.current + 1;
39
+
40
+ this.progress = Object.freeze({
41
+ ...this.progress,
42
+ current: nextCurrent,
43
+ label,
44
+ });
45
+ }
46
+
47
+ complete(): void {
48
+ if (!this.progress) {
49
+ return;
50
+ }
51
+
52
+ const completedLabel = this.phaseName ? `${this.phaseName} complete` : "Complete";
53
+ this.progress = Object.freeze({
54
+ ...this.progress,
55
+ current: this.progress.total,
56
+ label: completedLabel,
57
+ detail: "Completed",
58
+ });
59
+
60
+ void this.notificationManager?.success(
61
+ "Phase complete",
62
+ `${this.phaseName ?? "Phase"} finished.`,
63
+ );
64
+ }
65
+
66
+ getProgress(): ProgressUpdate | null {
67
+ return this.progress;
68
+ }
69
+
70
+ formatProgress(): string {
71
+ if (!this.progress) {
72
+ return "No active phase";
73
+ }
74
+
75
+ return `[${this.progress.current}/${this.progress.total}] ${this.progress.label}...`;
76
+ }
77
+ }
@@ -1,7 +1,32 @@
1
1
  import type { SessionEvents } from "../observability/event-store";
2
2
  import type { PipelineState } from "../orchestrator/types";
3
3
 
4
+ export interface SessionSummaryData {
5
+ readonly sessionId: string;
6
+ readonly startedAt: string;
7
+ readonly endedAt: string;
8
+ readonly tokensUsed: number;
9
+ readonly phasesCompleted: readonly string[];
10
+ readonly errorsEncountered: number;
11
+ readonly tasksCompleted: number;
12
+ readonly lessonsLearned: readonly string[];
13
+ }
14
+
4
15
  export function generateSessionSummary(
16
+ sessionData: SessionEvents | SessionSummaryData | undefined,
17
+ pipelineState?: PipelineState | null,
18
+ ): string {
19
+ if (isStructuredSessionSummaryData(sessionData) && pipelineState === undefined) {
20
+ return generateStructuredSessionSummary(sessionData);
21
+ }
22
+
23
+ return generatePipelineSessionSummary(
24
+ sessionData as SessionEvents | undefined,
25
+ pipelineState ?? null,
26
+ );
27
+ }
28
+
29
+ function generatePipelineSessionSummary(
5
30
  sessionData: SessionEvents | undefined,
6
31
  pipelineState: PipelineState | null,
7
32
  ): string {
@@ -54,3 +79,45 @@ export function generateSessionSummary(
54
79
 
55
80
  return sections.join("\n").trim();
56
81
  }
82
+
83
+ function generateStructuredSessionSummary(data: SessionSummaryData): string {
84
+ const sections = [
85
+ `# Session Summary: ${data.sessionId}`,
86
+ "",
87
+ "## Overview",
88
+ `- Started: ${data.startedAt}`,
89
+ `- Ended: ${data.endedAt}`,
90
+ `- Tokens Used: ${data.tokensUsed.toLocaleString()}`,
91
+ `- Tasks Completed: ${data.tasksCompleted}`,
92
+ `- Errors Encountered: ${data.errorsEncountered}`,
93
+ "",
94
+ "## Phases Completed",
95
+ ...(data.phasesCompleted.length > 0
96
+ ? data.phasesCompleted.map((phase) => `- ${phase}`)
97
+ : ["- None"]),
98
+ "",
99
+ "## Lessons Learned",
100
+ ...(data.lessonsLearned.length > 0
101
+ ? data.lessonsLearned.map((lesson) => `- ${lesson}`)
102
+ : ["- None recorded"]),
103
+ ];
104
+
105
+ return sections.join("\n").trim();
106
+ }
107
+
108
+ function isStructuredSessionSummaryData(
109
+ value: SessionEvents | SessionSummaryData | undefined,
110
+ ): value is SessionSummaryData {
111
+ if (!value) {
112
+ return false;
113
+ }
114
+
115
+ return (
116
+ "endedAt" in value &&
117
+ "tokensUsed" in value &&
118
+ "phasesCompleted" in value &&
119
+ "errorsEncountered" in value &&
120
+ "tasksCompleted" in value &&
121
+ "lessonsLearned" in value
122
+ );
123
+ }
@@ -0,0 +1,109 @@
1
+ export interface TaskStatusItem {
2
+ readonly id: string;
3
+ readonly description: string;
4
+ readonly status: string;
5
+ readonly createdAt: string;
6
+ }
7
+
8
+ export interface TaskStatusDisplayOptions {
9
+ readonly maxDisplayCount?: number;
10
+ }
11
+
12
+ const DEFAULT_MAX_DISPLAY_COUNT = 5;
13
+ const ACTIVE_STATUSES = new Set(["running"]);
14
+ const QUEUED_STATUSES = new Set(["pending"]);
15
+ const COMPLETED_STATUSES = new Set(["completed"]);
16
+
17
+ export class TaskStatusDisplay {
18
+ private readonly maxDisplayCount: number;
19
+ private tasks: readonly TaskStatusItem[] = [];
20
+
21
+ constructor(options: TaskStatusDisplayOptions = {}) {
22
+ this.maxDisplayCount = options.maxDisplayCount ?? DEFAULT_MAX_DISPLAY_COUNT;
23
+ }
24
+
25
+ updateTasks(tasks: readonly TaskStatusItem[]): void {
26
+ this.tasks = [...tasks];
27
+ }
28
+
29
+ formatStatus(): string {
30
+ const activeCount = this.getActiveCount();
31
+ const queueDepth = this.getQueueDepth();
32
+ const visibleTasks = this.getDisplayTasks();
33
+
34
+ const lines = [`Background Tasks: ${activeCount} active, ${queueDepth} queued`];
35
+
36
+ for (const task of visibleTasks) {
37
+ const age = formatAge(task.createdAt);
38
+ const ageSuffix = age ? ` (${age})` : "";
39
+ lines.push(`- [${task.status}] ${task.description}${ageSuffix}`);
40
+ }
41
+
42
+ return lines.join("\n");
43
+ }
44
+
45
+ getActiveCount(): number {
46
+ return this.tasks.filter((task) => ACTIVE_STATUSES.has(task.status)).length;
47
+ }
48
+
49
+ getQueueDepth(): number {
50
+ return this.tasks.filter((task) => QUEUED_STATUSES.has(task.status)).length;
51
+ }
52
+
53
+ getRecentCompletions(limit = this.maxDisplayCount): readonly TaskStatusItem[] {
54
+ return this.tasks
55
+ .filter((task) => COMPLETED_STATUSES.has(task.status))
56
+ .toSorted((left, right) => toTimestamp(right.createdAt) - toTimestamp(left.createdAt))
57
+ .slice(0, limit);
58
+ }
59
+
60
+ private getDisplayTasks(): readonly TaskStatusItem[] {
61
+ return this.tasks
62
+ .toSorted((left, right) => {
63
+ const leftPriority = getStatusPriority(left.status);
64
+ const rightPriority = getStatusPriority(right.status);
65
+ if (leftPriority !== rightPriority) {
66
+ return leftPriority - rightPriority;
67
+ }
68
+ return toTimestamp(right.createdAt) - toTimestamp(left.createdAt);
69
+ })
70
+ .slice(0, this.maxDisplayCount);
71
+ }
72
+ }
73
+
74
+ function getStatusPriority(status: string): number {
75
+ if (ACTIVE_STATUSES.has(status)) {
76
+ return 0;
77
+ }
78
+
79
+ if (QUEUED_STATUSES.has(status)) {
80
+ return 1;
81
+ }
82
+
83
+ if (COMPLETED_STATUSES.has(status)) {
84
+ return 2;
85
+ }
86
+
87
+ return 3;
88
+ }
89
+
90
+ function toTimestamp(value: string): number {
91
+ const timestamp = Date.parse(value);
92
+ return Number.isNaN(timestamp) ? 0 : timestamp;
93
+ }
94
+
95
+ function formatAge(createdAt: string): string | null {
96
+ const createdAtMs = toTimestamp(createdAt);
97
+ if (createdAtMs === 0) {
98
+ return null;
99
+ }
100
+
101
+ const elapsedMs = Math.max(0, Date.now() - createdAtMs);
102
+ const elapsedMinutes = Math.floor(elapsedMs / 60_000);
103
+ if (elapsedMinutes > 0) {
104
+ return `${elapsedMinutes}m ago`;
105
+ }
106
+
107
+ const elapsedSeconds = Math.floor(elapsedMs / 1_000);
108
+ return `${elapsedSeconds}s ago`;
109
+ }
@@ -0,0 +1,24 @@
1
+ export type ToastVariant = "info" | "success" | "warning" | "error";
2
+
3
+ export interface ToastOptions {
4
+ readonly title: string;
5
+ readonly message: string;
6
+ readonly variant: ToastVariant;
7
+ readonly duration?: number;
8
+ }
9
+
10
+ export interface NotificationSink {
11
+ showToast(
12
+ title: string,
13
+ message: string,
14
+ variant: ToastVariant,
15
+ duration?: number,
16
+ ): Promise<void>;
17
+ }
18
+
19
+ export interface ProgressUpdate {
20
+ readonly current: number;
21
+ readonly total: number;
22
+ readonly label: string;
23
+ readonly detail?: string;
24
+ }