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.
Files changed (217) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/agents/analyst.md +1 -1
  3. package/agents/cold-verifier.md +3 -1
  4. package/agents/critic.md +1 -1
  5. package/agents/executor.md +1 -1
  6. package/agents/explorer.md +1 -1
  7. package/agents/planner.md +1 -1
  8. package/agents/reviewer.md +1 -1
  9. package/agents/security-reviewer.md +1 -1
  10. package/agents/test-engineer.md +1 -1
  11. package/agents/verifier.md +1 -1
  12. package/agents/writer.md +1 -1
  13. package/dist/index.mjs +65838 -60331
  14. package/docs/README.md +2 -0
  15. package/docs/actions-reference.md +31 -0
  16. package/docs/commands-reference.md +17 -6
  17. package/docs/resource-formats.md +13 -0
  18. package/package.json +4 -2
  19. package/schema.json +15 -1
  20. package/scripts/resource-sampler.mjs +36 -2
  21. package/skills/requirements-to-task-packet/SKILL.md +26 -0
  22. package/skills/widget-rendering/SKILL.md +7 -7
  23. package/src/agents/agent-config.ts +2 -1
  24. package/src/agents/discover-agents.ts +23 -14
  25. package/src/config/config-merge.ts +183 -0
  26. package/src/config/config-validation.ts +681 -0
  27. package/src/config/config.ts +22 -864
  28. package/src/config/defaults.ts +35 -1
  29. package/src/config/drift-detector.ts +1 -1
  30. package/src/config/env-vars.ts +691 -0
  31. package/src/config/role-tools.ts +8 -8
  32. package/src/config/sanitize-project-config.ts +172 -0
  33. package/src/config/types.ts +30 -0
  34. package/src/extension/async-notifier.ts +25 -2
  35. package/src/extension/crew-vibes/config.ts +2 -1
  36. package/src/extension/plan-orchestrate.ts +132 -0
  37. package/src/extension/registration/commands/dashboard.ts +158 -0
  38. package/src/extension/registration/commands/index.ts +35 -0
  39. package/src/extension/registration/commands/manage.ts +303 -0
  40. package/src/extension/registration/commands/run.ts +214 -0
  41. package/src/extension/registration/commands/shared.ts +631 -0
  42. package/src/extension/registration/commands/status.ts +60 -0
  43. package/src/extension/registration/commands.ts +13 -1224
  44. package/src/extension/registration/lifecycle-handlers.ts +126 -15
  45. package/src/extension/registration/subagent-tools.ts +218 -9
  46. package/src/extension/rpc-hmac.ts +5 -3
  47. package/src/extension/team-tool/api/heartbeat.ts +47 -10
  48. package/src/extension/team-tool/api/plan-approval.ts +9 -0
  49. package/src/extension/team-tool/api/task-claims.ts +109 -40
  50. package/src/extension/team-tool/cancel.ts +84 -50
  51. package/src/extension/team-tool/dispatch/index.ts +1 -0
  52. package/src/extension/team-tool/dispatch/run.ts +4 -1
  53. package/src/extension/team-tool/doctor.ts +103 -1
  54. package/src/extension/team-tool/orchestrate.ts +66 -1
  55. package/src/extension/team-tool/plans.ts +192 -0
  56. package/src/extension/team-tool/respond.ts +197 -65
  57. package/src/extension/team-tool/run-deadline.ts +16 -1
  58. package/src/extension/team-tool/run-intent.ts +63 -0
  59. package/src/extension/team-tool/run.ts +48 -18
  60. package/src/extension/team-tool/status.ts +84 -26
  61. package/src/extension/team-tool.ts +11 -2
  62. package/src/hooks/registry.ts +1 -6
  63. package/src/i18n.ts +9 -0
  64. package/src/prompt/prompt-runtime.ts +521 -2
  65. package/src/prompt/worker-events-channel.ts +173 -0
  66. package/src/runtime/README.md +8 -8
  67. package/src/runtime/async-runner.ts +7 -3
  68. package/src/runtime/background-runner.ts +42 -14
  69. package/src/runtime/broker/broker-issuer.ts +9 -2
  70. package/src/runtime/broker/crew-broker-tokens.ts +43 -6
  71. package/src/runtime/broker/crew-broker.ts +838 -10
  72. package/src/runtime/broker/wait-status-cache.ts +157 -0
  73. package/src/runtime/budget-enforcement.ts +281 -0
  74. package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
  75. package/src/runtime/child-pi/child-pi-timers.ts +324 -0
  76. package/src/runtime/child-pi/child-pi.ts +97 -201
  77. package/src/runtime/child-pi/mock-fixtures.ts +16 -2
  78. package/src/runtime/crew-agent-records.ts +259 -14
  79. package/src/runtime/delegate-spawn.ts +148 -0
  80. package/src/runtime/deterministic-ast.ts +2 -1
  81. package/src/runtime/dispatch-batch.ts +945 -0
  82. package/src/runtime/finalize-run.ts +557 -0
  83. package/src/runtime/goal-workflow/adaptive-plan.ts +87 -13
  84. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +3 -2
  85. package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
  86. package/src/runtime/group-join.ts +11 -125
  87. package/src/runtime/live-session/live-session-runtime.ts +26 -1
  88. package/src/runtime/merge-gate.ts +7 -1
  89. package/src/runtime/merge-loop.ts +130 -0
  90. package/src/runtime/model/model-budget-summary.ts +53 -0
  91. package/src/runtime/model/model-fallback.ts +32 -2
  92. package/src/runtime/model/pi-args.ts +10 -0
  93. package/src/runtime/model/provider-extensions.ts +10 -0
  94. package/src/runtime/orphan-worker-registry.ts +1 -1
  95. package/src/runtime/output/output-validator.ts +45 -0
  96. package/src/runtime/parent-guard.ts +3 -1
  97. package/src/runtime/peer-dep.ts +2 -1
  98. package/src/runtime/per-write-validator.ts +0 -5
  99. package/src/runtime/pi-spawn.ts +61 -15
  100. package/src/runtime/plan-approval.ts +125 -0
  101. package/src/runtime/plan-replan.ts +151 -0
  102. package/src/runtime/recovery/checkpoint.ts +0 -18
  103. package/src/runtime/recovery/crash-recovery.ts +95 -46
  104. package/src/runtime/scheduler-context.ts +98 -0
  105. package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
  106. package/src/runtime/scheduling/global-worker-cap.ts +2 -1
  107. package/src/runtime/scheduling/nested-slots.ts +70 -0
  108. package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
  109. package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
  110. package/src/runtime/settings-store.ts +219 -0
  111. package/src/runtime/spawn-policy.ts +217 -0
  112. package/src/runtime/stale-reconciler.ts +87 -6
  113. package/src/runtime/subagent-manager.ts +25 -1
  114. package/src/runtime/task-output-context.ts +230 -9
  115. package/src/runtime/task-packet.ts +23 -1
  116. package/src/runtime/task-runner/child-executor.ts +106 -7
  117. package/src/runtime/task-runner/post-execution.ts +125 -1
  118. package/src/runtime/task-runner/pre-execution.ts +39 -1
  119. package/src/runtime/task-runner/prompt-builder.ts +51 -1
  120. package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
  121. package/src/runtime/task-runner/spec-evidence.ts +403 -0
  122. package/src/runtime/task-runner/state-helpers.ts +26 -24
  123. package/src/runtime/task-runner.ts +11 -0
  124. package/src/runtime/team-runner.ts +129 -1671
  125. package/src/runtime/verification/spec-sandbox.ts +255 -0
  126. package/src/runtime/verification/verification-gates.ts +3 -2
  127. package/src/runtime/verification/verification-worktree.ts +2 -1
  128. package/src/runtime/workflow-phase-advance.ts +100 -0
  129. package/src/runtime/workspace-tree.ts +9 -0
  130. package/src/schema/config-schema.ts +65 -25
  131. package/src/schema/sensitive-config-paths.ts +64 -0
  132. package/src/schema/team-tool-schema.ts +13 -3
  133. package/src/state/README.md +4 -10
  134. package/src/state/atomic-write.ts +20 -3
  135. package/src/state/contracts.ts +38 -0
  136. package/src/state/coordination/mailbox.ts +12 -2
  137. package/src/state/event-log/cursor.ts +223 -0
  138. package/src/state/event-log/event-log-rotation.ts +12 -4
  139. package/src/state/event-log/event-log.ts +152 -369
  140. package/src/state/event-log/sequence-cache.ts +373 -0
  141. package/src/state/event-log/worker-atomic-writer.ts +2 -1
  142. package/src/state/stores/active-run-registry.ts +3 -2
  143. package/src/state/stores/manifest-io.ts +237 -0
  144. package/src/state/stores/ownership-map.ts +162 -0
  145. package/src/state/stores/plan-store.ts +241 -0
  146. package/src/state/stores/run-cache.ts +0 -90
  147. package/src/state/stores/spec-store.ts +189 -0
  148. package/src/state/stores/state-store.ts +66 -229
  149. package/src/state/types.ts +197 -0
  150. package/src/ui/dashboard-panes/plan-pane.ts +136 -0
  151. package/src/ui/dashboard-panes/progress-pane.ts +6 -0
  152. package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
  153. package/src/ui/heartbeat-aggregator.ts +9 -1
  154. package/src/ui/keybinding-map.ts +54 -13
  155. package/src/ui/powerbar-publisher.ts +52 -1
  156. package/src/ui/run-dashboard.ts +31 -5
  157. package/src/ui/run-snapshot-cache.ts +57 -30
  158. package/src/ui/snapshot-types.ts +6 -1
  159. package/src/ui/widget/widget-renderer.ts +9 -1
  160. package/src/utils/file-coalescer.ts +0 -4
  161. package/src/utils/fs-errno.ts +66 -0
  162. package/src/utils/fs-watch.ts +1 -1
  163. package/src/utils/internal-error.ts +3 -1
  164. package/src/utils/paths.ts +11 -3
  165. package/src/utils/task-name-generator.ts +1 -8
  166. package/src/workflows/discover-workflows.ts +19 -2
  167. package/src/workflows/validate-workflow.ts +7 -1
  168. package/src/workflows/workflow-config.ts +10 -0
  169. package/src/workflows/workflow-serializer.ts +3 -0
  170. package/src/worktree/worktree-manager.ts +22 -0
  171. package/src/agents/agent-search.ts +0 -98
  172. package/src/benchmark/benchmark-runner.ts +0 -313
  173. package/src/benchmark/feedback-loop.ts +0 -73
  174. package/src/config/resilient-parser.ts +0 -117
  175. package/src/extension/crew-vibes/cat-frames.ts +0 -18
  176. package/src/extension/result-watcher.ts +0 -139
  177. package/src/observability/exporters/prometheus-exporter.ts +0 -54
  178. package/src/observability/metric-retention.ts +0 -64
  179. package/src/runtime/compaction/compaction-summary.ts +0 -278
  180. package/src/runtime/errors/crew-errors.ts +0 -162
  181. package/src/runtime/live-session/intercom-bridge.ts +0 -187
  182. package/src/runtime/loop-gates.ts +0 -128
  183. package/src/runtime/metric-parser.ts +0 -36
  184. package/src/runtime/output/stream-preview.ts +0 -184
  185. package/src/runtime/output/tool-progress.ts +0 -278
  186. package/src/runtime/phase-tracker.ts +0 -385
  187. package/src/runtime/pipeline-runner.ts +0 -523
  188. package/src/runtime/process/process-lifecycle.ts +0 -491
  189. package/src/runtime/recovery/retry-runner.ts +0 -330
  190. package/src/runtime/run-drift.ts +0 -219
  191. package/src/runtime/task-quality.ts +0 -199
  192. package/src/runtime/task-runner/run-projection.ts +0 -128
  193. package/src/runtime/verification/post-checks.ts +0 -142
  194. package/src/state/coordination/schedule.ts +0 -166
  195. package/src/state/event-log/jsonl-writer.ts +0 -115
  196. package/src/state/hook-instinct-bridge.ts +0 -94
  197. package/src/state/hook-integrations.ts +0 -51
  198. package/src/state/session-state-map.ts +0 -51
  199. package/src/state/stores/blob-store.ts +0 -308
  200. package/src/state/stores/instinct-store.ts +0 -275
  201. package/src/state/stores/observation-store.ts +0 -176
  202. package/src/state/tiered-eval.ts +0 -480
  203. package/src/state/types-eval.ts +0 -58
  204. package/src/tools/safe-bash-extension.ts +0 -54
  205. package/src/tools/safe-bash.ts +0 -505
  206. package/src/ui/agent-management-overlay.ts +0 -160
  207. package/src/ui/crew-footer.ts +0 -102
  208. package/src/ui/crew-select-list.ts +0 -114
  209. package/src/ui/dashboard-panes/capability-pane.ts +0 -77
  210. package/src/ui/transcript-entries.ts +0 -256
  211. package/src/utils/conflict-detect.ts +0 -721
  212. package/src/utils/fingerprint.ts +0 -180
  213. package/src/utils/gh-protocol.ts +0 -556
  214. package/src/utils/project-detector.ts +0 -160
  215. package/src/utils/sse-parser.ts +0 -131
  216. package/src/workflows/cost-estimator.ts +0 -34
  217. 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
- console.warn(
159
- `[discover-workflows] Stripping preStepScript from project workflow '${name}' step '${step.id}': '${step.preStepScript}' (F-02: project-sourced pre-step scripts are not allowed for RCE prevention)`,
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)) errors.push(`Step '${step.id}' references unknown team role '${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
- }