pi-crew 0.9.68 → 0.10.1
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/CHANGELOG.md +124 -0
- package/agents/analyst.md +1 -1
- package/agents/cold-verifier.md +3 -1
- package/agents/critic.md +1 -1
- package/agents/executor.md +1 -1
- package/agents/explorer.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/agents/writer.md +1 -1
- package/dist/index.mjs +65838 -60331
- package/docs/README.md +2 -0
- package/docs/actions-reference.md +31 -0
- package/docs/commands-reference.md +17 -6
- package/docs/resource-formats.md +13 -0
- package/package.json +4 -2
- package/schema.json +15 -1
- package/scripts/resource-sampler.mjs +36 -2
- package/skills/requirements-to-task-packet/SKILL.md +26 -0
- package/skills/widget-rendering/SKILL.md +7 -7
- package/src/agents/agent-config.ts +2 -1
- package/src/agents/discover-agents.ts +23 -14
- package/src/config/config-merge.ts +183 -0
- package/src/config/config-validation.ts +681 -0
- package/src/config/config.ts +22 -864
- package/src/config/defaults.ts +35 -1
- package/src/config/drift-detector.ts +1 -1
- package/src/config/env-vars.ts +691 -0
- package/src/config/role-tools.ts +8 -8
- package/src/config/sanitize-project-config.ts +172 -0
- package/src/config/types.ts +30 -0
- package/src/extension/async-notifier.ts +25 -2
- package/src/extension/crew-vibes/config.ts +2 -1
- package/src/extension/plan-orchestrate.ts +132 -0
- package/src/extension/registration/commands/dashboard.ts +158 -0
- package/src/extension/registration/commands/index.ts +35 -0
- package/src/extension/registration/commands/manage.ts +303 -0
- package/src/extension/registration/commands/run.ts +214 -0
- package/src/extension/registration/commands/shared.ts +631 -0
- package/src/extension/registration/commands/status.ts +60 -0
- package/src/extension/registration/commands.ts +13 -1224
- package/src/extension/registration/lifecycle-handlers.ts +126 -15
- package/src/extension/registration/subagent-tools.ts +218 -9
- package/src/extension/rpc-hmac.ts +5 -3
- package/src/extension/team-tool/api/heartbeat.ts +47 -10
- package/src/extension/team-tool/api/plan-approval.ts +9 -0
- package/src/extension/team-tool/api/task-claims.ts +109 -40
- package/src/extension/team-tool/cancel.ts +84 -50
- package/src/extension/team-tool/dispatch/index.ts +1 -0
- package/src/extension/team-tool/dispatch/run.ts +4 -1
- package/src/extension/team-tool/doctor.ts +103 -1
- package/src/extension/team-tool/orchestrate.ts +66 -1
- package/src/extension/team-tool/plans.ts +192 -0
- package/src/extension/team-tool/respond.ts +197 -65
- package/src/extension/team-tool/run-deadline.ts +16 -1
- package/src/extension/team-tool/run-intent.ts +63 -0
- package/src/extension/team-tool/run.ts +48 -18
- package/src/extension/team-tool/status.ts +84 -26
- package/src/extension/team-tool.ts +11 -2
- package/src/hooks/registry.ts +1 -6
- package/src/i18n.ts +9 -0
- package/src/prompt/prompt-runtime.ts +521 -2
- package/src/prompt/worker-events-channel.ts +173 -0
- package/src/runtime/README.md +8 -8
- package/src/runtime/async-runner.ts +7 -3
- package/src/runtime/background-runner.ts +42 -14
- package/src/runtime/broker/broker-issuer.ts +9 -2
- package/src/runtime/broker/crew-broker-tokens.ts +43 -6
- package/src/runtime/broker/crew-broker.ts +838 -10
- package/src/runtime/broker/wait-status-cache.ts +157 -0
- package/src/runtime/budget-enforcement.ts +281 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
- package/src/runtime/child-pi/child-pi-timers.ts +324 -0
- package/src/runtime/child-pi/child-pi.ts +97 -201
- package/src/runtime/child-pi/mock-fixtures.ts +16 -2
- package/src/runtime/crew-agent-records.ts +259 -14
- package/src/runtime/delegate-spawn.ts +148 -0
- package/src/runtime/deterministic-ast.ts +2 -1
- package/src/runtime/dispatch-batch.ts +945 -0
- package/src/runtime/finalize-run.ts +557 -0
- package/src/runtime/goal-workflow/adaptive-plan.ts +87 -13
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +3 -2
- package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
- package/src/runtime/group-join.ts +11 -125
- package/src/runtime/live-session/live-session-runtime.ts +26 -1
- package/src/runtime/merge-gate.ts +7 -1
- package/src/runtime/merge-loop.ts +130 -0
- package/src/runtime/model/model-budget-summary.ts +53 -0
- package/src/runtime/model/model-fallback.ts +32 -2
- package/src/runtime/model/pi-args.ts +10 -0
- package/src/runtime/model/provider-extensions.ts +10 -0
- package/src/runtime/orphan-worker-registry.ts +1 -1
- package/src/runtime/output/output-validator.ts +45 -0
- package/src/runtime/parent-guard.ts +3 -1
- package/src/runtime/peer-dep.ts +2 -1
- package/src/runtime/per-write-validator.ts +0 -5
- package/src/runtime/pi-spawn.ts +61 -15
- package/src/runtime/plan-approval.ts +125 -0
- package/src/runtime/plan-replan.ts +151 -0
- package/src/runtime/recovery/checkpoint.ts +0 -18
- package/src/runtime/recovery/crash-recovery.ts +95 -46
- package/src/runtime/scheduler-context.ts +98 -0
- package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
- package/src/runtime/scheduling/global-worker-cap.ts +2 -1
- package/src/runtime/scheduling/nested-slots.ts +70 -0
- package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
- package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
- package/src/runtime/settings-store.ts +219 -0
- package/src/runtime/spawn-policy.ts +217 -0
- package/src/runtime/stale-reconciler.ts +87 -6
- package/src/runtime/subagent-manager.ts +25 -1
- package/src/runtime/task-output-context.ts +230 -9
- package/src/runtime/task-packet.ts +23 -1
- package/src/runtime/task-runner/child-executor.ts +106 -7
- package/src/runtime/task-runner/post-execution.ts +125 -1
- package/src/runtime/task-runner/pre-execution.ts +39 -1
- package/src/runtime/task-runner/prompt-builder.ts +51 -1
- package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
- package/src/runtime/task-runner/spec-evidence.ts +403 -0
- package/src/runtime/task-runner/state-helpers.ts +26 -24
- package/src/runtime/task-runner.ts +11 -0
- package/src/runtime/team-runner.ts +129 -1671
- package/src/runtime/verification/spec-sandbox.ts +255 -0
- package/src/runtime/verification/verification-gates.ts +3 -2
- package/src/runtime/verification/verification-worktree.ts +2 -1
- package/src/runtime/workflow-phase-advance.ts +100 -0
- package/src/runtime/workspace-tree.ts +9 -0
- package/src/schema/config-schema.ts +65 -25
- package/src/schema/sensitive-config-paths.ts +64 -0
- package/src/schema/team-tool-schema.ts +13 -3
- package/src/state/README.md +4 -10
- package/src/state/atomic-write.ts +20 -3
- package/src/state/contracts.ts +38 -0
- package/src/state/coordination/mailbox.ts +12 -2
- package/src/state/event-log/cursor.ts +223 -0
- package/src/state/event-log/event-log-rotation.ts +12 -4
- package/src/state/event-log/event-log.ts +152 -369
- package/src/state/event-log/sequence-cache.ts +373 -0
- package/src/state/event-log/worker-atomic-writer.ts +2 -1
- package/src/state/stores/active-run-registry.ts +3 -2
- package/src/state/stores/manifest-io.ts +237 -0
- package/src/state/stores/ownership-map.ts +162 -0
- package/src/state/stores/plan-store.ts +241 -0
- package/src/state/stores/run-cache.ts +0 -90
- package/src/state/stores/spec-store.ts +189 -0
- package/src/state/stores/state-store.ts +66 -229
- package/src/state/types.ts +197 -0
- package/src/ui/dashboard-panes/plan-pane.ts +136 -0
- package/src/ui/dashboard-panes/progress-pane.ts +6 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
- package/src/ui/heartbeat-aggregator.ts +9 -1
- package/src/ui/keybinding-map.ts +54 -13
- package/src/ui/powerbar-publisher.ts +52 -1
- package/src/ui/run-dashboard.ts +31 -5
- package/src/ui/run-snapshot-cache.ts +57 -30
- package/src/ui/snapshot-types.ts +6 -1
- package/src/ui/widget/widget-renderer.ts +9 -1
- package/src/utils/file-coalescer.ts +0 -4
- package/src/utils/fs-errno.ts +66 -0
- package/src/utils/fs-watch.ts +1 -1
- package/src/utils/internal-error.ts +3 -1
- package/src/utils/paths.ts +11 -3
- package/src/utils/task-name-generator.ts +1 -8
- package/src/workflows/discover-workflows.ts +19 -2
- package/src/workflows/validate-workflow.ts +7 -1
- package/src/workflows/workflow-config.ts +10 -0
- package/src/workflows/workflow-serializer.ts +3 -0
- package/src/worktree/worktree-manager.ts +22 -0
- package/src/agents/agent-search.ts +0 -98
- package/src/benchmark/benchmark-runner.ts +0 -313
- package/src/benchmark/feedback-loop.ts +0 -73
- package/src/config/resilient-parser.ts +0 -117
- package/src/extension/crew-vibes/cat-frames.ts +0 -18
- package/src/extension/result-watcher.ts +0 -139
- package/src/observability/exporters/prometheus-exporter.ts +0 -54
- package/src/observability/metric-retention.ts +0 -64
- package/src/runtime/compaction/compaction-summary.ts +0 -278
- package/src/runtime/errors/crew-errors.ts +0 -162
- package/src/runtime/live-session/intercom-bridge.ts +0 -187
- package/src/runtime/loop-gates.ts +0 -128
- package/src/runtime/metric-parser.ts +0 -36
- package/src/runtime/output/stream-preview.ts +0 -184
- package/src/runtime/output/tool-progress.ts +0 -278
- package/src/runtime/phase-tracker.ts +0 -385
- package/src/runtime/pipeline-runner.ts +0 -523
- package/src/runtime/process/process-lifecycle.ts +0 -491
- package/src/runtime/recovery/retry-runner.ts +0 -330
- package/src/runtime/run-drift.ts +0 -219
- package/src/runtime/task-quality.ts +0 -199
- package/src/runtime/task-runner/run-projection.ts +0 -128
- package/src/runtime/verification/post-checks.ts +0 -142
- package/src/state/coordination/schedule.ts +0 -166
- package/src/state/event-log/jsonl-writer.ts +0 -115
- package/src/state/hook-instinct-bridge.ts +0 -94
- package/src/state/hook-integrations.ts +0 -51
- package/src/state/session-state-map.ts +0 -51
- package/src/state/stores/blob-store.ts +0 -308
- package/src/state/stores/instinct-store.ts +0 -275
- package/src/state/stores/observation-store.ts +0 -176
- package/src/state/tiered-eval.ts +0 -480
- package/src/state/types-eval.ts +0 -58
- package/src/tools/safe-bash-extension.ts +0 -54
- package/src/tools/safe-bash.ts +0 -505
- package/src/ui/agent-management-overlay.ts +0 -160
- package/src/ui/crew-footer.ts +0 -102
- package/src/ui/crew-select-list.ts +0 -114
- package/src/ui/dashboard-panes/capability-pane.ts +0 -77
- package/src/ui/transcript-entries.ts +0 -256
- package/src/utils/conflict-detect.ts +0 -721
- package/src/utils/fingerprint.ts +0 -180
- package/src/utils/gh-protocol.ts +0 -556
- package/src/utils/project-detector.ts +0 -160
- package/src/utils/sse-parser.ts +0 -131
- package/src/workflows/cost-estimator.ts +0 -34
- package/src/workflows/intermediate-store.ts +0 -166
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { ResourceSource } from "../agents/agent-config.ts";
|
|
4
4
|
import { parseCsv, parseFrontmatter } from "../utils/frontmatter.ts";
|
|
5
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
5
6
|
import { packageRoot, projectCrewRoot, userPiRoot } from "../utils/paths.ts";
|
|
6
7
|
import type { WorkflowConfig, WorkflowStep } from "./workflow-config.ts";
|
|
7
8
|
|
|
@@ -24,6 +25,8 @@ const STEP_CONFIG_KEYS = new Set([
|
|
|
24
25
|
"verify",
|
|
25
26
|
"task",
|
|
26
27
|
"seedPaths",
|
|
28
|
+
"specRefs",
|
|
29
|
+
"specStrict",
|
|
27
30
|
"preStepScript",
|
|
28
31
|
"preStepArgs",
|
|
29
32
|
"preStepTimeout",
|
|
@@ -68,6 +71,11 @@ function parseStepSection(id: string, body: string): WorkflowStep | undefined {
|
|
|
68
71
|
worktree: config.worktree === "true" ? true : config.worktree === "false" ? false : undefined,
|
|
69
72
|
verify: config.verify === "true" ? true : config.verify === "false" ? false : undefined,
|
|
70
73
|
seedPaths: parseCsv(config.seedPaths) || undefined,
|
|
74
|
+
specRefs: parseCsv(config.specRefs) || undefined,
|
|
75
|
+
// Tri-state (round-1 P1-2): ABSENT = undefined so the workflow-level flag
|
|
76
|
+
// survives the `step.specStrict ?? workflow.specStrict` dispatch merge; an
|
|
77
|
+
// explicit `false` stays false (per-step opt-out of a strict workflow).
|
|
78
|
+
specStrict: config.specStrict === undefined ? undefined : config.specStrict === "true" || config.specStrict === "1",
|
|
71
79
|
preStepScript: config.preStepScript || undefined,
|
|
72
80
|
preStepArgs: parseCsv(config.preStepArgs) || undefined,
|
|
73
81
|
preStepTimeout: parseOptionalInteger(config.preStepTimeout) ?? undefined,
|
|
@@ -155,8 +163,16 @@ function parseWorkflowFile(filePath: string, source: ResourceSource): WorkflowCo
|
|
|
155
163
|
// runtime, but stripping here prevents the script value from even reaching
|
|
156
164
|
// downstream consumers (coalesce-tasks, policy-engine, etc.).
|
|
157
165
|
if (source === "project" && step.preStepScript) {
|
|
158
|
-
|
|
159
|
-
|
|
166
|
+
// F-02 redaction: NEVER log the preStepScript body — project workflows
|
|
167
|
+
// are untrusted input and the script may contain secrets. Log name,
|
|
168
|
+
// step id, and script length only.
|
|
169
|
+
logInternalError(
|
|
170
|
+
"discover-workflows",
|
|
171
|
+
new Error(
|
|
172
|
+
`Stripping preStepScript from project workflow '${name}' step '${step.id}' — script body redacted (${step.preStepScript.length} chars) (F-02: project-sourced pre-step scripts are not allowed for RCE prevention)`,
|
|
173
|
+
),
|
|
174
|
+
undefined,
|
|
175
|
+
"warn",
|
|
160
176
|
);
|
|
161
177
|
step.preStepScript = undefined;
|
|
162
178
|
step.preStepArgs = undefined;
|
|
@@ -174,6 +190,7 @@ function parseWorkflowFile(filePath: string, source: ResourceSource): WorkflowCo
|
|
|
174
190
|
maxConcurrency: parseOptionalInteger(frontmatter.maxConcurrency),
|
|
175
191
|
topology: parseTopology(frontmatter.topology),
|
|
176
192
|
coalesceMicroTasks: parseOptionalBoolean(frontmatter.coalesceMicroTasks),
|
|
193
|
+
specStrict: parseOptionalBoolean(frontmatter.specStrict),
|
|
177
194
|
steps,
|
|
178
195
|
};
|
|
179
196
|
} catch {
|
|
@@ -9,7 +9,13 @@ export function validateWorkflowForTeam(workflow: WorkflowConfig, team: TeamConf
|
|
|
9
9
|
for (const step of workflow.steps) {
|
|
10
10
|
if (stepIds.has(step.id)) errors.push(`Duplicate workflow step id '${step.id}'.`);
|
|
11
11
|
stepIds.add(step.id);
|
|
12
|
-
if (!roles.has(step.role))
|
|
12
|
+
if (!roles.has(step.role)) {
|
|
13
|
+
// F19-9: the distill adapter needs roles the default team lacks
|
|
14
|
+
// (analyst/critic) — hint the correct invocation instead of a bare
|
|
15
|
+
// unknown-role error.
|
|
16
|
+
const distillHint = workflow.name === "distill" ? " (run with team='implementation' — needs analyst+critic roles)" : "";
|
|
17
|
+
errors.push(`Step '${step.id}' references unknown team role '${step.role}'.${distillHint}`);
|
|
18
|
+
}
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
for (const step of workflow.steps) {
|
|
@@ -22,6 +22,12 @@ export interface WorkflowStep {
|
|
|
22
22
|
/** Per-step files to overlay into the worktree (in addition to global worktree.seedPaths).
|
|
23
23
|
* Useful when only certain steps need access to local drafts or scripts. */
|
|
24
24
|
seedPaths?: string[];
|
|
25
|
+
/** T4/R6 (ADR-6): workspace SpecRecord ids this step is held to; frozen
|
|
26
|
+
* into the task packet at dispatch (specStrict per workflow frontmatter). */
|
|
27
|
+
specRefs?: string[];
|
|
28
|
+
/** T4/R6 (ADR-6 §7): per-step strict-mode opt-in. Normally inherited from
|
|
29
|
+
* the workflow-level frontmatter flag (merged at dispatch). */
|
|
30
|
+
specStrict?: boolean;
|
|
25
31
|
/** Path to a deterministic script to run before dispatching the LLM worker.
|
|
26
32
|
* Script stdout is injected into the worker's prompt as context.
|
|
27
33
|
* Pattern origin: Understand-Anything deterministic pre-step pattern. */
|
|
@@ -65,6 +71,10 @@ export interface WorkflowConfig {
|
|
|
65
71
|
* in v0.9.17 unless the workflow author opts in. See
|
|
66
72
|
* src/runtime/scheduling/coalesce-tasks.ts. */
|
|
67
73
|
coalesceMicroTasks?: boolean;
|
|
74
|
+
/** T4/R6 (ADR-6 §7): strict spec mode opt-in (frontmatter `specStrict: true`).
|
|
75
|
+
* Requires a verifier-role step (reject-start, §5) and fails the write-gate
|
|
76
|
+
* on coverage gaps or machine-check failures (§4). */
|
|
77
|
+
specStrict?: boolean;
|
|
68
78
|
}
|
|
69
79
|
|
|
70
80
|
/** A dynamic workflow (runtime === "dynamic"). steps is empty — the script is the source of truth. */
|
|
@@ -14,6 +14,8 @@ function serializeStep(step: WorkflowStep): string[] {
|
|
|
14
14
|
if (step.progress !== undefined) lines.push(`progress: ${step.progress ? "true" : "false"}`);
|
|
15
15
|
if (step.worktree !== undefined) lines.push(`worktree: ${step.worktree ? "true" : "false"}`);
|
|
16
16
|
if (step.verify !== undefined) lines.push(`verify: ${step.verify ? "true" : "false"}`);
|
|
17
|
+
if (step.specRefs?.length) lines.push(`specRefs: ${step.specRefs.join(", ")}`);
|
|
18
|
+
if (step.specStrict !== undefined && step.specStrict !== false) lines.push("specStrict: true");
|
|
17
19
|
lines.push("", step.task.trim(), "");
|
|
18
20
|
return lines;
|
|
19
21
|
}
|
|
@@ -24,6 +26,7 @@ export function serializeWorkflow(workflow: WorkflowConfig): string {
|
|
|
24
26
|
`name: ${workflow.name}`,
|
|
25
27
|
`description: ${workflow.description}`,
|
|
26
28
|
...(workflow.maxConcurrency !== undefined ? [`maxConcurrency: ${workflow.maxConcurrency}`] : []),
|
|
29
|
+
...(workflow.specStrict !== undefined ? [`specStrict: ${workflow.specStrict ? "true" : "false"}`] : []),
|
|
27
30
|
"---",
|
|
28
31
|
"",
|
|
29
32
|
...workflow.steps.flatMap(serializeStep),
|
|
@@ -146,6 +146,12 @@ export function assertCleanLeader(repoRoot: string): void {
|
|
|
146
146
|
|
|
147
147
|
// --- Async versions ---
|
|
148
148
|
|
|
149
|
+
// R5-L1 (Round 5 LOW-1): the exported clear*() fns below stay unwired by
|
|
150
|
+
// leader decision (rewiring run-start/lock-release paths is riskier than the
|
|
151
|
+
// leak); the FIFO caps bound these caches instead.
|
|
152
|
+
const MAX_GIT_ROOT_CACHE = 256;
|
|
153
|
+
const MAX_CLEAN_LEADER_CACHE = 256;
|
|
154
|
+
|
|
149
155
|
/** Cache for findGitRoot results keyed by cwd. Cleared per-run. */
|
|
150
156
|
const _gitRootCache = new Map<string, string>();
|
|
151
157
|
|
|
@@ -159,6 +165,10 @@ export async function findGitRootAsync(cwd: string): Promise<string> {
|
|
|
159
165
|
if (cached) return cached;
|
|
160
166
|
const root = await gitAsync(cwd, ["rev-parse", "--show-toplevel"]);
|
|
161
167
|
_gitRootCache.set(cwd, root);
|
|
168
|
+
if (_gitRootCache.size > MAX_GIT_ROOT_CACHE) {
|
|
169
|
+
const oldest = _gitRootCache.keys().next().value;
|
|
170
|
+
if (oldest !== undefined) _gitRootCache.delete(oldest);
|
|
171
|
+
}
|
|
162
172
|
return root;
|
|
163
173
|
}
|
|
164
174
|
|
|
@@ -179,6 +189,10 @@ export async function assertCleanLeaderAsync(repoRoot: string): Promise<void> {
|
|
|
179
189
|
throw new Error("Worktree mode requires a clean leader repository. Commit/stash changes or use workspaceMode: 'single'.");
|
|
180
190
|
}
|
|
181
191
|
_cleanLeaderCache.add(repoRoot);
|
|
192
|
+
if (_cleanLeaderCache.size > MAX_CLEAN_LEADER_CACHE) {
|
|
193
|
+
const oldest = _cleanLeaderCache.values().next().value;
|
|
194
|
+
if (oldest !== undefined) _cleanLeaderCache.delete(oldest);
|
|
195
|
+
}
|
|
182
196
|
}
|
|
183
197
|
|
|
184
198
|
function linkNodeModulesIfPresent(repoRoot: string, worktreePath: string): boolean {
|
|
@@ -469,6 +483,10 @@ async function branchExistsAsync(repoRoot: string, branch: string): Promise<{ lo
|
|
|
469
483
|
// run no new stale worktrees appear, so one prune suffices).
|
|
470
484
|
const _prunedRepos = new Set<string>();
|
|
471
485
|
const _pruneInFlight = new Map<string, Promise<void>>();
|
|
486
|
+
// R5-L2 (Round 5 LOW-2): FIFO cap — dedupe-only Set, never cleared in
|
|
487
|
+
// production; eviction just means a re-prune for that repo later, which is
|
|
488
|
+
// harmless (prune is idempotent and coalesced).
|
|
489
|
+
const MAX_PRUNED_REPOS = 128;
|
|
472
490
|
async function pruneStaleWorktreesAsync(repoRoot: string): Promise<void> {
|
|
473
491
|
if (_prunedRepos.has(repoRoot)) return;
|
|
474
492
|
const inFlight = _pruneInFlight.get(repoRoot);
|
|
@@ -483,6 +501,10 @@ async function pruneStaleWorktreesAsync(repoRoot: string): Promise<void> {
|
|
|
483
501
|
/* best-effort */
|
|
484
502
|
}
|
|
485
503
|
_prunedRepos.add(repoRoot);
|
|
504
|
+
if (_prunedRepos.size > MAX_PRUNED_REPOS) {
|
|
505
|
+
const oldest = _prunedRepos.values().next().value;
|
|
506
|
+
if (oldest !== undefined) _prunedRepos.delete(oldest);
|
|
507
|
+
}
|
|
486
508
|
_pruneInFlight.delete(repoRoot);
|
|
487
509
|
})();
|
|
488
510
|
_pruneInFlight.set(repoRoot, p);
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import type { AgentConfig } from "./agent-config.ts";
|
|
2
|
-
|
|
3
|
-
// ─── BM25 Agent Search ──────────────────────────────────────────────────────
|
|
4
|
-
// Lightweight BM25 search over agent descriptors for task-to-agent matching.
|
|
5
|
-
// Based on the same BM25 algorithm used in oh-my-pi's tool-index.ts.
|
|
6
|
-
|
|
7
|
-
export interface AgentSearchDocument {
|
|
8
|
-
agent: AgentConfig;
|
|
9
|
-
termFrequencies: Map<string, number>;
|
|
10
|
-
length: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface AgentSearchIndex {
|
|
14
|
-
documents: AgentSearchDocument[];
|
|
15
|
-
averageLength: number;
|
|
16
|
-
documentFrequencies: Map<string, number>;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface AgentSearchResult {
|
|
20
|
-
agent: AgentConfig;
|
|
21
|
-
score: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const BM25_K1 = 1.2;
|
|
25
|
-
const BM25_B = 0.75;
|
|
26
|
-
const FIELD_WEIGHTS = {
|
|
27
|
-
name: 6,
|
|
28
|
-
description: 2,
|
|
29
|
-
role: 3,
|
|
30
|
-
} as const;
|
|
31
|
-
|
|
32
|
-
function tokenize(value: string): string[] {
|
|
33
|
-
return value
|
|
34
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
35
|
-
.replace(/[^a-zA-Z0-9]+/g, " ")
|
|
36
|
-
.toLowerCase()
|
|
37
|
-
.trim()
|
|
38
|
-
.split(/\s+/)
|
|
39
|
-
.filter((token) => token.length > 0);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function addWeightedTokens(termFrequencies: Map<string, number>, value: string | undefined, weight: number): void {
|
|
43
|
-
if (!value) return;
|
|
44
|
-
for (const token of tokenize(value)) {
|
|
45
|
-
termFrequencies.set(token, (termFrequencies.get(token) ?? 0) + weight);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function buildAgentSearchDocument(agent: AgentConfig): AgentSearchDocument {
|
|
50
|
-
const termFrequencies = new Map<string, number>();
|
|
51
|
-
addWeightedTokens(termFrequencies, agent.name, FIELD_WEIGHTS.name);
|
|
52
|
-
addWeightedTokens(termFrequencies, agent.description, FIELD_WEIGHTS.description);
|
|
53
|
-
// Role from agent name heuristic
|
|
54
|
-
const roleHint = agent.name?.replace(/[-_]/g, " ") ?? "";
|
|
55
|
-
addWeightedTokens(termFrequencies, roleHint, FIELD_WEIGHTS.role);
|
|
56
|
-
const length = Array.from(termFrequencies.values()).reduce((sum, value) => sum + value, 0);
|
|
57
|
-
return { agent, termFrequencies, length };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function buildAgentSearchIndex(agents: Iterable<AgentConfig>): AgentSearchIndex {
|
|
61
|
-
const documents = Array.from(agents, buildAgentSearchDocument);
|
|
62
|
-
const averageLength = documents.reduce((sum, document) => sum + document.length, 0) / documents.length || 1;
|
|
63
|
-
const documentFrequencies = new Map<string, number>();
|
|
64
|
-
for (const document of documents) {
|
|
65
|
-
for (const token of new Set(document.termFrequencies.keys())) {
|
|
66
|
-
documentFrequencies.set(token, (documentFrequencies.get(token) ?? 0) + 1);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return { documents, averageLength, documentFrequencies };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function searchAgents(index: AgentSearchIndex, query: string, limit: number): AgentSearchResult[] {
|
|
73
|
-
const queryTokens = tokenize(query);
|
|
74
|
-
if (queryTokens.length === 0) return [];
|
|
75
|
-
if (index.documents.length === 0) return [];
|
|
76
|
-
|
|
77
|
-
const queryTermCounts = new Map<string, number>();
|
|
78
|
-
for (const token of queryTokens) {
|
|
79
|
-
queryTermCounts.set(token, (queryTermCounts.get(token) ?? 0) + 1);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return index.documents
|
|
83
|
-
.map((document) => {
|
|
84
|
-
let score = 0;
|
|
85
|
-
for (const [token, queryTermCount] of queryTermCounts) {
|
|
86
|
-
const termFrequency = document.termFrequencies.get(token) ?? 0;
|
|
87
|
-
if (termFrequency === 0) continue;
|
|
88
|
-
const documentFrequency = index.documentFrequencies.get(token) ?? 0;
|
|
89
|
-
const idf = Math.log(1 + (index.documents.length - documentFrequency + 0.5) / (documentFrequency + 0.5));
|
|
90
|
-
const normalization = BM25_K1 * (1 - BM25_B + BM25_B * (document.length / index.averageLength));
|
|
91
|
-
score += queryTermCount * idf * ((termFrequency * (BM25_K1 + 1)) / (termFrequency + normalization));
|
|
92
|
-
}
|
|
93
|
-
return { agent: document.agent, score };
|
|
94
|
-
})
|
|
95
|
-
.filter((result) => result.score > 0)
|
|
96
|
-
.sort((left, right) => right.score - left.score || left.agent.name.localeCompare(right.agent.name))
|
|
97
|
-
.slice(0, limit);
|
|
98
|
-
}
|
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Benchmark runner - agent-eval inspired benchmarking system.
|
|
3
|
-
* Provides tiered evaluation for workflow tasks.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { execFileSync } from "node:child_process";
|
|
7
|
-
|
|
8
|
-
export interface BenchmarkJudge {
|
|
9
|
-
type: "pytest" | "grep" | "command";
|
|
10
|
-
command?: string;
|
|
11
|
-
pattern?: string;
|
|
12
|
-
description: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface BenchmarkTask {
|
|
16
|
-
id: string;
|
|
17
|
-
name: string;
|
|
18
|
-
prompt: string;
|
|
19
|
-
judges: BenchmarkJudge[];
|
|
20
|
-
/** Optional task-type label used for aggregate metrics grouping. */
|
|
21
|
-
taskType?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface BenchmarkResult {
|
|
25
|
-
taskId: string;
|
|
26
|
-
/** Task-type label for aggregation grouping. */
|
|
27
|
-
taskType?: string;
|
|
28
|
-
passed: boolean;
|
|
29
|
-
judgeResults: { description: string; passed: boolean; output?: string }[];
|
|
30
|
-
durationMs: number;
|
|
31
|
-
/** Estimated cost in dollars (0 if not tracked). */
|
|
32
|
-
cost: number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Validate command against allowlist to prevent shell injection.
|
|
37
|
-
* Only allows specific safe commands with arguments.
|
|
38
|
-
*/
|
|
39
|
-
/**
|
|
40
|
-
* Validate command against allowlist to prevent shell injection.
|
|
41
|
-
* Uses comprehensive shell metacharacter blocking similar to safe-bash.ts.
|
|
42
|
-
*/
|
|
43
|
-
function validateCommand(command: string): void {
|
|
44
|
-
// Basic allowlist - must start with an allowed command.
|
|
45
|
-
// SECURITY (H-7): `npx`/`node` were removed because they enable arbitrary code
|
|
46
|
-
// execution without any shell metacharacter (e.g. `npx --yes evil-package`
|
|
47
|
-
// or `node -e "require('fs')…"`). Use `npm test`/`npm run …` instead of raw
|
|
48
|
-
// `node`/`npx` in benchmark task definitions.
|
|
49
|
-
// `echo` is allowed because the metachar blocker (validateGateCommand) rejects
|
|
50
|
-
// command substitution (`$(...)`, backticks), so `echo $(evil)` cannot run;
|
|
51
|
-
// bare `echo …` only prints. It's the canonical exit-0 command used in
|
|
52
|
-
// benchmark fixtures across Linux/macOS/Windows(sh).
|
|
53
|
-
const allowlist = /^(pytest|grep|npm test|cargo test|cargo clippy|echo) /;
|
|
54
|
-
if (!allowlist.test(command)) {
|
|
55
|
-
throw new Error(`Command not allowed: ${command}. Only pytest, grep, npm test, cargo test/clippy, echo allowed.`);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Block shell metacharacters after command name
|
|
59
|
-
const afterCommand = command.substring(command.indexOf(" ") + 1);
|
|
60
|
-
|
|
61
|
-
// Block dangerous shell metacharacters
|
|
62
|
-
const dangerousPatterns = [
|
|
63
|
-
/[;&|`$(){}[\]<>\\]/, // Shell metacharacters
|
|
64
|
-
/\$\([^)]*\)/, // Command substitution $(...)
|
|
65
|
-
/`[^`]*`/, // Backtick command substitution
|
|
66
|
-
/\|/, // Pipe
|
|
67
|
-
/&&/, // And
|
|
68
|
-
/\|\|/, // Or
|
|
69
|
-
/>>/, // Append redirect
|
|
70
|
-
/2>&1/, // stderr redirect
|
|
71
|
-
/>/, // Output redirect
|
|
72
|
-
/</, // Input redirect
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
for (const pattern of dangerousPatterns) {
|
|
76
|
-
if (pattern.test(afterCommand)) {
|
|
77
|
-
throw new Error(`Shell metacharacters not allowed in command arguments`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Run a single benchmark task with tiered judges.
|
|
84
|
-
* Tier 1: pytest (fast, deterministic)
|
|
85
|
-
* Tier 2: grep pattern matching
|
|
86
|
-
* Tier 3: command execution
|
|
87
|
-
* Fails fast on first tier failure.
|
|
88
|
-
*/
|
|
89
|
-
function splitCommand(command: string): { program: string; args: string[] } {
|
|
90
|
-
// Naive split on whitespace. validateCommand already rejects shell
|
|
91
|
-
// metacharacters, so a simple split is safe.
|
|
92
|
-
const parts = command.trim().split(/\s+/);
|
|
93
|
-
if (parts.length === 0) {
|
|
94
|
-
throw new Error("Empty command");
|
|
95
|
-
}
|
|
96
|
-
return { program: parts[0]!, args: parts.slice(1) };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export async function runBenchmark(task: BenchmarkTask): Promise<BenchmarkResult> {
|
|
100
|
-
const startTime = Date.now();
|
|
101
|
-
const judgeResults: BenchmarkResult["judgeResults"] = [];
|
|
102
|
-
|
|
103
|
-
for (const judge of task.judges) {
|
|
104
|
-
try {
|
|
105
|
-
let passed = false;
|
|
106
|
-
let output: string | undefined;
|
|
107
|
-
|
|
108
|
-
if (judge.type === "pytest" && judge.command) {
|
|
109
|
-
// Validate command before execution (defense-in-depth)
|
|
110
|
-
validateCommand(judge.command);
|
|
111
|
-
// Use execFileSync to avoid shell parsing. validateCommand
|
|
112
|
-
// already rejects metacharacters, so a simple split is safe.
|
|
113
|
-
const { program, args } = splitCommand(judge.command);
|
|
114
|
-
// Tier 1: pytest - fast deterministic check
|
|
115
|
-
output = execFileSync(program, args, {
|
|
116
|
-
timeout: 5000,
|
|
117
|
-
encoding: "utf-8",
|
|
118
|
-
cwd: process.cwd(),
|
|
119
|
-
});
|
|
120
|
-
// Look for pytest summary line with passed count
|
|
121
|
-
passed = output.includes("passed");
|
|
122
|
-
} else if (judge.type === "grep" && judge.pattern && judge.command) {
|
|
123
|
-
// Validate command before execution (defense-in-depth)
|
|
124
|
-
validateCommand(judge.command);
|
|
125
|
-
const { program, args } = splitCommand(judge.command);
|
|
126
|
-
// Tier 2: grep pattern matching
|
|
127
|
-
output = execFileSync(program, args, {
|
|
128
|
-
timeout: 5000,
|
|
129
|
-
encoding: "utf-8",
|
|
130
|
-
cwd: process.cwd(),
|
|
131
|
-
});
|
|
132
|
-
passed = output.includes(judge.pattern);
|
|
133
|
-
} else if (judge.type === "command" && judge.command) {
|
|
134
|
-
// Validate command before execution (defense-in-depth)
|
|
135
|
-
validateCommand(judge.command);
|
|
136
|
-
const { program, args } = splitCommand(judge.command);
|
|
137
|
-
// Tier 3: command execution
|
|
138
|
-
output = execFileSync(program, args, {
|
|
139
|
-
timeout: 10000,
|
|
140
|
-
encoding: "utf-8",
|
|
141
|
-
cwd: process.cwd(),
|
|
142
|
-
});
|
|
143
|
-
passed = true; // Command succeeded = pass
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
judgeResults.push({
|
|
147
|
-
description: judge.description,
|
|
148
|
-
passed: passed ?? false,
|
|
149
|
-
output,
|
|
150
|
-
});
|
|
151
|
-
} catch (e: unknown) {
|
|
152
|
-
const error = e as { message?: string };
|
|
153
|
-
judgeResults.push({
|
|
154
|
-
description: judge.description,
|
|
155
|
-
passed: false,
|
|
156
|
-
output: error.message ?? String(e),
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
taskId: task.id,
|
|
163
|
-
passed: judgeResults.every((j) => j.passed),
|
|
164
|
-
judgeResults,
|
|
165
|
-
durationMs: Date.now() - startTime,
|
|
166
|
-
cost: 0,
|
|
167
|
-
taskType: task.taskType,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Aggregate metrics computed over a group of benchmark results for a single task type.
|
|
173
|
-
*/
|
|
174
|
-
export interface BenchmarkMetrics {
|
|
175
|
-
taskType: string;
|
|
176
|
-
totalTasks: number;
|
|
177
|
-
passedTasks: number;
|
|
178
|
-
/** Ratio of passed/total (0–1). */
|
|
179
|
-
passRate: number;
|
|
180
|
-
/** Mean execution duration in milliseconds. */
|
|
181
|
-
avgTimeMs: number;
|
|
182
|
-
/** Total estimated cost in dollars across all tasks. */
|
|
183
|
-
totalCost: number;
|
|
184
|
-
/** Mean cost in dollars per task. */
|
|
185
|
-
avgCost: number;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Per-task-type aggregate metrics map.
|
|
190
|
-
* Keys are task-type labels; "__default__" is used when a task has no label.
|
|
191
|
-
*/
|
|
192
|
-
export type AggregateMetrics = Record<string, BenchmarkMetrics>;
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Run multiple benchmark tasks and aggregate results.
|
|
196
|
-
*
|
|
197
|
-
* @param tasks - Benchmark tasks to execute. Each task may carry a `taskType` label.
|
|
198
|
-
* @param taskTypes - Optional subset of task-type labels to run. If provided, only tasks
|
|
199
|
-
* whose `taskType` is in this set will be executed. If omitted, all tasks run.
|
|
200
|
-
*/
|
|
201
|
-
export async function runBenchmarkSuite(
|
|
202
|
-
tasks: BenchmarkTask[],
|
|
203
|
-
taskTypes?: string[],
|
|
204
|
-
): Promise<{
|
|
205
|
-
results: BenchmarkResult[];
|
|
206
|
-
totalPassed: number;
|
|
207
|
-
totalFailed: number;
|
|
208
|
-
totalDurationMs: number;
|
|
209
|
-
totalCost: number;
|
|
210
|
-
}> {
|
|
211
|
-
const filtered = taskTypes ? tasks.filter((t) => t.taskType && taskTypes.includes(t.taskType)) : tasks;
|
|
212
|
-
|
|
213
|
-
const results: BenchmarkResult[] = [];
|
|
214
|
-
|
|
215
|
-
for (const task of filtered) {
|
|
216
|
-
const result = await runBenchmark(task);
|
|
217
|
-
results.push(result);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const totalPassed = results.filter((r) => r.passed).length;
|
|
221
|
-
const totalFailed = results.length - totalPassed;
|
|
222
|
-
const totalDurationMs = results.reduce((a, b) => a + b.durationMs, 0);
|
|
223
|
-
const totalCost = results.reduce((a, b) => a + b.cost, 0);
|
|
224
|
-
|
|
225
|
-
return { results, totalPassed, totalFailed, totalDurationMs, totalCost };
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Aggregate benchmark results into per-task-type metrics.
|
|
230
|
-
*
|
|
231
|
-
* @param results - Raw benchmark results (may include any task-type mix).
|
|
232
|
-
* @returns A map from task-type label to `BenchmarkMetrics`. Tasks with no label
|
|
233
|
-
* are grouped under `"__default__"`.
|
|
234
|
-
*/
|
|
235
|
-
export function aggregateBenchmarkMetrics(results: BenchmarkResult[]): AggregateMetrics {
|
|
236
|
-
const buckets: Record<string, BenchmarkResult[]> = {};
|
|
237
|
-
|
|
238
|
-
for (const result of results) {
|
|
239
|
-
const key = result.taskType ?? "__default__";
|
|
240
|
-
if (!buckets[key]) buckets[key] = [];
|
|
241
|
-
buckets[key].push(result);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const metrics: AggregateMetrics = {};
|
|
245
|
-
|
|
246
|
-
for (const [taskType, group] of Object.entries(buckets)) {
|
|
247
|
-
const totalTasks = group.length;
|
|
248
|
-
const passedTasks = group.filter((r) => r.passed).length;
|
|
249
|
-
const passRate = totalTasks > 0 ? passedTasks / totalTasks : 0;
|
|
250
|
-
const avgTimeMs = totalTasks > 0 ? group.reduce((s, r) => s + r.durationMs, 0) / totalTasks : 0;
|
|
251
|
-
const totalCost = group.reduce((s, r) => s + r.cost, 0);
|
|
252
|
-
const avgCost = totalTasks > 0 ? totalCost / totalTasks : 0;
|
|
253
|
-
|
|
254
|
-
metrics[taskType] = {
|
|
255
|
-
taskType,
|
|
256
|
-
totalTasks,
|
|
257
|
-
passedTasks,
|
|
258
|
-
passRate: Math.round(passRate * 1000) / 1000,
|
|
259
|
-
avgTimeMs: Math.round(avgTimeMs),
|
|
260
|
-
totalCost: Math.round(totalCost * 1e6) / 1e6,
|
|
261
|
-
avgCost: Math.round(avgCost * 1e6) / 1e6,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return metrics;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Generate a markdown comparison table for benchmark results including per-type aggregates.
|
|
270
|
-
*
|
|
271
|
-
* @param results - Benchmark results to report.
|
|
272
|
-
* @param includeTaskTypeComparison - When true (default), appends a per-task-type aggregate table.
|
|
273
|
-
*/
|
|
274
|
-
export function generateBenchmarkReport(results: BenchmarkResult[], includeTaskTypeComparison = true): string {
|
|
275
|
-
const lines: string[] = ["# Benchmark Results", ""];
|
|
276
|
-
|
|
277
|
-
lines.push("| Task | Type | Status | Duration | Cost |");
|
|
278
|
-
lines.push("|------|------|--------|---------|------|");
|
|
279
|
-
|
|
280
|
-
for (const r of results) {
|
|
281
|
-
const status = r.passed ? "✅ PASS" : "❌ FAIL";
|
|
282
|
-
const type = r.taskType ?? "—";
|
|
283
|
-
const cost = r.cost > 0 ? `$${r.cost.toFixed(4)}` : "—";
|
|
284
|
-
lines.push(`| ${r.taskId} | ${type} | ${status} | ${r.durationMs}ms | ${cost} |`);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
lines.push("");
|
|
288
|
-
|
|
289
|
-
// Per-type aggregate table.
|
|
290
|
-
if (includeTaskTypeComparison && results.length > 0) {
|
|
291
|
-
const metrics = aggregateBenchmarkMetrics(results);
|
|
292
|
-
const types = Object.keys(metrics).sort();
|
|
293
|
-
|
|
294
|
-
if (types.length > 0) {
|
|
295
|
-
lines.push("## Per-Task-Type Comparison", "");
|
|
296
|
-
lines.push("| Task Type | Total | Passed | Pass Rate | Avg Time | Avg Cost |");
|
|
297
|
-
lines.push("|-----------|-------|--------|-----------|----------|---------|");
|
|
298
|
-
|
|
299
|
-
for (const t of types) {
|
|
300
|
-
const m = metrics[t];
|
|
301
|
-
const passRatePct = `${(m.passRate * 100).toFixed(1)}%`;
|
|
302
|
-
const avgCostStr = m.avgCost > 0 ? `$${m.avgCost.toFixed(4)}` : "—";
|
|
303
|
-
lines.push(`| ${m.taskType} | ${m.totalTasks} | ${m.passedTasks} | ${passRatePct} | ${m.avgTimeMs}ms | ${avgCostStr} |`);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const passed = results.filter((r) => r.passed).length;
|
|
309
|
-
lines.push("");
|
|
310
|
-
lines.push(`**Total: ${passed}/${results.length} passed**`);
|
|
311
|
-
|
|
312
|
-
return lines.join("\n");
|
|
313
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Feedback loop - continuous improvement cycle: evaluate → learn → apply → re-evaluate
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { RunMetrics } from "../state/stores/run-metrics.ts";
|
|
6
|
-
|
|
7
|
-
/** @internal */
|
|
8
|
-
interface FeedbackLoopStats {
|
|
9
|
-
runsObserved: number;
|
|
10
|
-
avgSuccessRate: number;
|
|
11
|
-
recommendations: string[];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** @internal */
|
|
15
|
-
class FeedbackLoop {
|
|
16
|
-
private runs: RunMetrics[] = [];
|
|
17
|
-
private static readonly MAX_RUNS = 1000;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Record a run's metrics for learning.
|
|
21
|
-
*/
|
|
22
|
-
recordRun(metrics: RunMetrics): void {
|
|
23
|
-
this.runs.push(metrics);
|
|
24
|
-
// Trim to MAX_RUNS to prevent unbounded memory growth
|
|
25
|
-
if (this.runs.length > FeedbackLoop.MAX_RUNS) {
|
|
26
|
-
this.runs = this.runs.slice(-FeedbackLoop.MAX_RUNS);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Get current statistics and recommendations.
|
|
32
|
-
*/
|
|
33
|
-
getStats(): FeedbackLoopStats {
|
|
34
|
-
if (this.runs.length === 0) {
|
|
35
|
-
return {
|
|
36
|
-
runsObserved: 0,
|
|
37
|
-
avgSuccessRate: 0,
|
|
38
|
-
recommendations: ["No runs observed yet. Run some workflows to gather data."],
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const successRates = this.runs.map((r) => (r.taskCount > 0 ? r.completedCount / r.taskCount : 0));
|
|
43
|
-
const avg = successRates.reduce((a, b) => a + b, 0) / successRates.length;
|
|
44
|
-
|
|
45
|
-
const recommendations: string[] = [];
|
|
46
|
-
if (avg >= 0.9) {
|
|
47
|
-
recommendations.push(`High success rate (${(avg * 100).toFixed(0)}%). Current configuration is working well.`);
|
|
48
|
-
} else if (avg >= 0.7) {
|
|
49
|
-
recommendations.push(`Moderate success rate (${(avg * 100).toFixed(0)}%). Consider reviewing failed tasks for patterns.`);
|
|
50
|
-
} else {
|
|
51
|
-
recommendations.push(`Low success rate (${(avg * 100).toFixed(0)}%). Investigate failure patterns.`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Cost awareness
|
|
55
|
-
const avgCost = this.runs.reduce((a, b) => a + b.totalCost, 0) / this.runs.length;
|
|
56
|
-
if (avgCost > 10) {
|
|
57
|
-
recommendations.push(`Average cost per run: $${avgCost.toFixed(2)}. Consider optimization.`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
runsObserved: this.runs.length,
|
|
62
|
-
avgSuccessRate: avg,
|
|
63
|
-
recommendations,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Clear recorded runs.
|
|
69
|
-
*/
|
|
70
|
-
clear(): void {
|
|
71
|
-
this.runs = [];
|
|
72
|
-
}
|
|
73
|
-
}
|