@bugmole/cli 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bugmole.env.example +20 -0
- package/LICENSE +7 -0
- package/README.md +293 -0
- package/TESTING.md +117 -0
- package/bugmole.config.yaml +39 -0
- package/package.json +85 -0
- package/scripts/billing/paypal-setup.mjs +121 -0
- package/scripts/bugmole-continue.ts +318 -0
- package/scripts/bugmole-init.ts +188 -0
- package/scripts/bugmole.cjs +17 -0
- package/scripts/bugmole.test.ts +344 -0
- package/scripts/bugmole.ts +657 -0
- package/scripts/ensure-maestro.cjs +79 -0
- package/scripts/ios-tunnel-keeper.sh +45 -0
- package/scripts/ios-wda-keeper.sh +66 -0
- package/scripts/sync-plan-catalog.d.mts +3 -0
- package/scripts/sync-plan-catalog.mjs +16 -0
- package/scripts/ui-parity-diff.py +65 -0
- package/scripts/ui-parity-requirements.txt +1 -0
- package/scripts/verify-manage-to-plans.mts +194 -0
- package/spec/app-ui-audit.schema.json +176 -0
- package/spec/blockers.yaml +79 -0
- package/spec/bugs.index.json +42 -0
- package/spec/design-dna.schema.json +38 -0
- package/spec/domain_rules.yaml +24 -0
- package/spec/flows/flow_manage-to-plans-64c3c61b.yaml +8 -0
- package/spec/flows/flow_screen-to-evidence-6c3ab309.yaml +7 -0
- package/spec/journey_graph.yaml +126 -0
- package/spec/journeys.graph.json +2618 -0
- package/spec/plans/dashboard-smoke.flow.yaml +20 -0
- package/spec/plans/example.flow.yaml +99 -0
- package/spec/plans/flow_api-keys-to-logout-7396cd5d.flow.yaml +33 -0
- package/spec/plans/flow_api-keys-to-screen-157e10a6.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-audits-176c1eb8.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-blockers-633282c8.flow.yaml +112 -0
- package/spec/plans/flow_manage-to-devices-ad57e202.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-logout-86c54656.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-manage-resource-action-6325f99a.flow.yaml +185 -0
- package/spec/plans/flow_manage-to-parity-issues-01fa2579.flow.yaml +34 -0
- package/spec/plans/flow_manage-to-plans-64c3c61b.flow.yaml +123 -0
- package/spec/plans/flow_manage-to-screen-6857f915.flow.yaml +106 -0
- package/spec/plans/flow_manage-to-tasks-c5791c95.flow.yaml +33 -0
- package/spec/plans/flow_profile-to-screen-6045aa3d.flow.yaml +33 -0
- package/spec/plans/flow_register-to-api-auth-login-460a9cc8.flow.yaml +34 -0
- package/spec/plans/flow_screen-to-audits-07666356.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-blockers-091d258b.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-devices-cad72f75.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-evidence-6c3ab309.flow.yaml +126 -0
- package/spec/plans/flow_screen-to-parity-issues-2218e3ad.flow.yaml +34 -0
- package/spec/plans/flow_screen-to-plans-8232a94f.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-tasks-540671d1.flow.yaml +33 -0
- package/spec/plans/login.flow.yaml +22 -0
- package/spec/plans/owner-operations.flow.yaml +20 -0
- package/spec/project_config.yaml +55 -0
- package/spec/roles.yaml +30 -0
- package/spec/schema.md +394 -0
- package/spec/test-case-results.schema.json +62 -0
- package/spec/test-cases.schema.json +85 -0
- package/spec/ui-parity-audit.schema.json +194 -0
- package/spec/ui-reverse-engineering.schema.json +94 -0
- package/src/billing/plan-catalog.test.ts +46 -0
- package/src/billing/plan-catalog.ts +199 -0
- package/src/integrations/aws-sigv4.test.ts +42 -0
- package/src/integrations/aws-sigv4.ts +72 -0
- package/src/integrations/device-farm.ts +155 -0
- package/src/integrations/github-app.test.ts +57 -0
- package/src/integrations/github-app.ts +143 -0
- package/src/integrations/gitlab.ts +81 -0
- package/src/integrations/temp-email.test.ts +123 -0
- package/src/integrations/temp-email.ts +175 -0
- package/src/integrations/testflight-feedback.test.ts +51 -0
- package/src/integrations/testflight-feedback.ts +173 -0
- package/src/integrations/webdriver-client.ts +131 -0
- package/src/mcp/server.test.ts +1220 -0
- package/src/mcp/server.ts +3064 -0
- package/src/mcp/write-test-cases.test.ts +287 -0
- package/src/registry/api-key-client.ts +39 -0
- package/src/registry/control-plane-client.ts +212 -0
- package/src/registry/migrations/0001_registry.sql +47 -0
- package/src/registry/migrations/0002_device_authorizations.sql +23 -0
- package/src/registry/migrations/0003_project_environments.sql +25 -0
- package/src/registry/migrations/0004_device_authorization_email.sql +1 -0
- package/src/registry/migrations/0005_testing_control_plane.sql +55 -0
- package/src/registry/migrations/0006_workspaces.sql +36 -0
- package/src/registry/migrations/0007_project_apps.sql +26 -0
- package/src/registry/migrations/0008_agent_tasks.sql +30 -0
- package/src/registry/migrations/0009_journey_revisions.sql +17 -0
- package/src/registry/migrations/0010_agent_task_journey.sql +2 -0
- package/src/registry/migrations/0011_run_journey_revision.sql +1 -0
- package/src/registry/migrations/0012_canonical_flow_execution.sql +10 -0
- package/src/registry/migrations/0013_device_sessions.sql +22 -0
- package/src/registry/migrations/0014_agent_task_step.sql +1 -0
- package/src/registry/migrations/0015_agent_task_attempts.sql +6 -0
- package/src/registry/migrations/0016_github_issue_tracker.sql +54 -0
- package/src/registry/migrations/0017_run_targets.sql +7 -0
- package/src/registry/migrations/0018_run_fix_from.sql +4 -0
- package/src/registry/migrations/0019_workspace_flags.sql +9 -0
- package/src/registry/migrations/0020_orgs.sql +40 -0
- package/src/registry/migrations/0021_billing_core.sql +58 -0
- package/src/registry/migrations/0022_cloud_runners.sql +19 -0
- package/src/registry/migrations/0023_signup.sql +4 -0
- package/src/registry/migrations/0024_billing.sql +67 -0
- package/src/registry/migrations/0025_notifications.sql +47 -0
- package/src/registry/migrations/0026_repo_bindings.sql +28 -0
- package/src/registry/migrations/0027_feedback.sql +29 -0
- package/src/registry/migrations/0028_devices.sql +48 -0
- package/src/registry/migrations/0029_sso.sql +31 -0
- package/src/registry/migrations/0030_workspace_domains.sql +18 -0
- package/src/registry/migrations/0031_gitlab_and_teams.sql +45 -0
- package/src/registry/migrations/0032_personas.sql +15 -0
- package/src/registry/migrations/0033_bugmole_rename.sql +11 -0
- package/src/registry/task-scheduling.test.ts +100 -0
- package/src/registry/task-scheduling.ts +80 -0
- package/src/registry-worker/ai/platform-model.ts +77 -0
- package/src/registry-worker/ai/routes.test.ts +88 -0
- package/src/registry-worker/ai/routes.ts +90 -0
- package/src/registry-worker/artifacts.test.ts +98 -0
- package/src/registry-worker/artifacts.ts +85 -0
- package/src/registry-worker/billing/billing-core.test.ts +175 -0
- package/src/registry-worker/billing/checkout-routes.ts +209 -0
- package/src/registry-worker/billing/enforcement.ts +69 -0
- package/src/registry-worker/billing/entitlements.ts +108 -0
- package/src/registry-worker/billing/ledger.ts +186 -0
- package/src/registry-worker/billing/paypal/api.ts +259 -0
- package/src/registry-worker/billing/paypal/client.ts +91 -0
- package/src/registry-worker/billing/paypal/provider.ts +143 -0
- package/src/registry-worker/billing/paypal.test.ts +466 -0
- package/src/registry-worker/billing/provider.ts +114 -0
- package/src/registry-worker/billing/routes.ts +66 -0
- package/src/registry-worker/billing/subscriptions.ts +780 -0
- package/src/registry-worker/billing/thresholds.ts +107 -0
- package/src/registry-worker/core.ts +308 -0
- package/src/registry-worker/devices/devices.test.ts +185 -0
- package/src/registry-worker/devices/policy.ts +71 -0
- package/src/registry-worker/devices/routes.ts +453 -0
- package/src/registry-worker/domains/domains.test.ts +210 -0
- package/src/registry-worker/domains/routes.ts +139 -0
- package/src/registry-worker/domains.ts +88 -0
- package/src/registry-worker/email/sender.ts +75 -0
- package/src/registry-worker/env.d.ts +14716 -0
- package/src/registry-worker/features.ts +20 -0
- package/src/registry-worker/feedback/feedback.test.ts +230 -0
- package/src/registry-worker/feedback/format.ts +148 -0
- package/src/registry-worker/feedback/routes.ts +386 -0
- package/src/registry-worker/flags.ts +39 -0
- package/src/registry-worker/github/checks.test.ts +177 -0
- package/src/registry-worker/github/checks.ts +374 -0
- package/src/registry-worker/gitlab/checks.test.ts +141 -0
- package/src/registry-worker/gitlab/checks.ts +349 -0
- package/src/registry-worker/hooks.ts +54 -0
- package/src/registry-worker/index.ts +2077 -0
- package/src/registry-worker/jobs/index.ts +29 -0
- package/src/registry-worker/jobs/retention.ts +68 -0
- package/src/registry-worker/mcp/mcp.test.ts +355 -0
- package/src/registry-worker/mcp/routes.ts +215 -0
- package/src/registry-worker/mcp/token.ts +126 -0
- package/src/registry-worker/mcp/tools.ts +563 -0
- package/src/registry-worker/notifications/alerts.ts +212 -0
- package/src/registry-worker/notifications/notifications.test.ts +298 -0
- package/src/registry-worker/notifications/outbox.ts +83 -0
- package/src/registry-worker/notifications/routes.ts +280 -0
- package/src/registry-worker/notifications/secrets.ts +49 -0
- package/src/registry-worker/notifications/slack.ts +96 -0
- package/src/registry-worker/notifications/teams.ts +46 -0
- package/src/registry-worker/org/audit.ts +116 -0
- package/src/registry-worker/org/routes.test.ts +163 -0
- package/src/registry-worker/org/routes.ts +302 -0
- package/src/registry-worker/personas/personas.test.ts +78 -0
- package/src/registry-worker/personas/routes.ts +100 -0
- package/src/registry-worker/repo-triggers.ts +20 -0
- package/src/registry-worker/routes/index.ts +74 -0
- package/src/registry-worker/run-events.ts +24 -0
- package/src/registry-worker/runner/dispatch.ts +219 -0
- package/src/registry-worker/runner/jobs.ts +43 -0
- package/src/registry-worker/runner/metering.ts +82 -0
- package/src/registry-worker/runner/policy.ts +59 -0
- package/src/registry-worker/runner/routes.ts +171 -0
- package/src/registry-worker/runner/runner.test.ts +358 -0
- package/src/registry-worker/runner/tokens.ts +93 -0
- package/src/registry-worker/runs.test.ts +60 -0
- package/src/registry-worker/signup/policy.ts +57 -0
- package/src/registry-worker/signup/routes.ts +106 -0
- package/src/registry-worker/signup/signup.test.ts +81 -0
- package/src/registry-worker/sso/aegis.ts +141 -0
- package/src/registry-worker/sso/membership.ts +157 -0
- package/src/registry-worker/sso/routes.ts +458 -0
- package/src/registry-worker/sso/sso.test.ts +344 -0
- package/src/registry-worker/testing/d1-shim.ts +180 -0
- package/src/registry-worker/testing/harness.ts +137 -0
- package/src/runner-worker/index.ts +108 -0
- package/src/runtime/ai-analysis.ts +97 -0
- package/src/runtime/ai-exploration.test.ts +32 -0
- package/src/runtime/ai-exploration.ts +69 -0
- package/src/runtime/ai-repair.ts +74 -0
- package/src/runtime/ai-work.test.ts +99 -0
- package/src/runtime/android-screen-record.test.ts +75 -0
- package/src/runtime/android-screen-record.ts +192 -0
- package/src/runtime/app-understanding.test.ts +123 -0
- package/src/runtime/app-understanding.ts +201 -0
- package/src/runtime/appium-driver.test.ts +179 -0
- package/src/runtime/appium-driver.ts +295 -0
- package/src/runtime/blocker-resolution.test.ts +113 -0
- package/src/runtime/blocker-resolution.ts +111 -0
- package/src/runtime/browser-matrix.integration.test.ts +212 -0
- package/src/runtime/browser-matrix.test.ts +143 -0
- package/src/runtime/browser-matrix.ts +200 -0
- package/src/runtime/canonical-flow.test.ts +52 -0
- package/src/runtime/config-validate.ts +185 -0
- package/src/runtime/continuous-execution.ts +291 -0
- package/src/runtime/cursor-applescript.ts +573 -0
- package/src/runtime/cursor-cli-driver.test.ts +78 -0
- package/src/runtime/cursor-cli-driver.ts +156 -0
- package/src/runtime/cursor-driver-example.ts +117 -0
- package/src/runtime/cursor-driver-index.ts +65 -0
- package/src/runtime/cursor-driver-init.ts +277 -0
- package/src/runtime/cursor-driver-run.test.ts +15 -0
- package/src/runtime/cursor-driver-run.ts +323 -0
- package/src/runtime/cursor-driver.ts +332 -0
- package/src/runtime/cursor-llm-example.ts +90 -0
- package/src/runtime/cursor-llm.ts +206 -0
- package/src/runtime/cursor-mcp-monitor.ts +386 -0
- package/src/runtime/device-clouds/browserstack.ts +73 -0
- package/src/runtime/device-clouds/device-farm.ts +52 -0
- package/src/runtime/device-clouds/index.ts +92 -0
- package/src/runtime/device-clouds/kobiton.ts +70 -0
- package/src/runtime/device-clouds/targets.ts +44 -0
- package/src/runtime/device-clouds/types.ts +62 -0
- package/src/runtime/diff-proposal.ts +84 -0
- package/src/runtime/discovery-task.test.ts +29 -0
- package/src/runtime/discovery-task.ts +284 -0
- package/src/runtime/driver-recovery.ts +69 -0
- package/src/runtime/driver.ts +79 -0
- package/src/runtime/environment.test.ts +104 -0
- package/src/runtime/environment.ts +137 -0
- package/src/runtime/executor.test.ts +509 -0
- package/src/runtime/executor.ts +921 -0
- package/src/runtime/explorer.test.ts +101 -0
- package/src/runtime/explorer.ts +1013 -0
- package/src/runtime/failure-analysis.test.ts +111 -0
- package/src/runtime/failure-analysis.ts +272 -0
- package/src/runtime/fixtures/fake-maestro.sh +36 -0
- package/src/runtime/flow-language.test.ts +268 -0
- package/src/runtime/flow-language.ts +414 -0
- package/src/runtime/init-wizard.ts +354 -0
- package/src/runtime/ios-screen-record.test.ts +68 -0
- package/src/runtime/ios-screen-record.ts +155 -0
- package/src/runtime/journey-editor.ts +452 -0
- package/src/runtime/journey-evidence.test.ts +161 -0
- package/src/runtime/journey-evidence.ts +180 -0
- package/src/runtime/journey-graph.test.ts +257 -0
- package/src/runtime/journey-graph.ts +170 -0
- package/src/runtime/legacy-names.ts +32 -0
- package/src/runtime/llm-example.ts +105 -0
- package/src/runtime/llm.ts +527 -0
- package/src/runtime/local-browser.test.ts +45 -0
- package/src/runtime/local-browser.ts +48 -0
- package/src/runtime/local-registry-stub.test.ts +325 -0
- package/src/runtime/local-registry-stub.ts +803 -0
- package/src/runtime/maestro-driver.test.ts +84 -0
- package/src/runtime/maestro-driver.ts +209 -0
- package/src/runtime/mole-voice.ts +21 -0
- package/src/runtime/nav-crawl.test.ts +100 -0
- package/src/runtime/nav-crawl.ts +153 -0
- package/src/runtime/pipeline.test.ts +405 -0
- package/src/runtime/pipeline.ts +833 -0
- package/src/runtime/planner.test.ts +37 -0
- package/src/runtime/planner.ts +274 -0
- package/src/runtime/platform-ai.ts +76 -0
- package/src/runtime/playwright-driver.test.ts +93 -0
- package/src/runtime/playwright-driver.ts +620 -0
- package/src/runtime/project-spec.ts +140 -0
- package/src/runtime/record-run-verdicts.ts +68 -0
- package/src/runtime/reporter.test.ts +56 -0
- package/src/runtime/reporter.ts +158 -0
- package/src/runtime/reset.test.ts +44 -0
- package/src/runtime/reset.ts +61 -0
- package/src/runtime/reviewer.test.ts +73 -0
- package/src/runtime/reviewer.ts +158 -0
- package/src/runtime/run-job.ts +136 -0
- package/src/runtime/run-once.test.ts +207 -0
- package/src/runtime/run-once.ts +168 -0
- package/src/runtime/run.ts +132 -0
- package/src/runtime/screen-recording.ts +34 -0
- package/src/runtime/serve-gateway.test.ts +74 -0
- package/src/runtime/serve-gateway.ts +164 -0
- package/src/runtime/serve-worker.test.ts +23 -0
- package/src/runtime/serve-worker.ts +278 -0
- package/src/runtime/site-discovery.test.ts +168 -0
- package/src/runtime/site-discovery.ts +308 -0
- package/src/runtime/target-runner.ts +144 -0
- package/src/runtime/test-case-verdicts.test.ts +94 -0
- package/src/runtime/test-case-verdicts.ts +120 -0
- package/src/runtime/ui-reverse-engineering/coordinator.test.ts +59 -0
- package/src/runtime/ui-reverse-engineering/coordinator.ts +391 -0
- package/src/runtime/ui-reverse-engineering/types.ts +197 -0
- package/src/runtime/web-suite.test.ts +97 -0
- package/src/runtime/web-suite.ts +176 -0
- package/src/storage/create-object-store.ts +144 -0
- package/src/storage/keys.ts +34 -0
- package/src/storage/local-artifact-server.test.ts +314 -0
- package/src/storage/local-artifact-server.ts +357 -0
- package/src/storage/object-store.test.ts +28 -0
- package/src/storage/object-store.ts +101 -0
- package/src/storage/registry-object-store.ts +88 -0
- package/src/storage/remote-object-store.ts +104 -0
- package/src/storage/storage-directory.test.ts +43 -0
- package/src/storage/storage-directory.ts +24 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,3064 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { URL } from "node:url";
|
|
5
|
+
import * as readline from "node:readline";
|
|
6
|
+
import { createProjectObjectStore } from "../storage/create-object-store.js";
|
|
7
|
+
import { json, type ObjectStore } from "../storage/object-store.js";
|
|
8
|
+
import { projectPrefix } from "../storage/keys.js";
|
|
9
|
+
import { ControlPlaneClient, isClaimableTask } from "../registry/control-plane-client.js";
|
|
10
|
+
import { getEnvironmentContract, setEnvironmentContract } from "../runtime/environment.js";
|
|
11
|
+
import {
|
|
12
|
+
completeCoordinatorJob,
|
|
13
|
+
extractAtomicCandidates,
|
|
14
|
+
getCoordinatorJob,
|
|
15
|
+
getDesignDNA,
|
|
16
|
+
inferDesignDNA,
|
|
17
|
+
listReferenceInputs,
|
|
18
|
+
measureReference,
|
|
19
|
+
normalizeReferenceInput,
|
|
20
|
+
reconcileCoordinatorJob,
|
|
21
|
+
registerReferenceInput,
|
|
22
|
+
runCoordinatorPipeline,
|
|
23
|
+
startCoordinatorJob,
|
|
24
|
+
} from "../runtime/ui-reverse-engineering/coordinator.js";
|
|
25
|
+
import {
|
|
26
|
+
addJourneyStep,
|
|
27
|
+
addJourneyTransition,
|
|
28
|
+
createJourney,
|
|
29
|
+
reuseJourneyStep,
|
|
30
|
+
stabilizeJourney,
|
|
31
|
+
nextJourneyTask,
|
|
32
|
+
readJourneyGraph,
|
|
33
|
+
removeJourneyItem,
|
|
34
|
+
nextJourneyTaskWithClarification,
|
|
35
|
+
updateJourney,
|
|
36
|
+
writeJourneyGraph,
|
|
37
|
+
} from "../runtime/journey-editor.js";
|
|
38
|
+
import {
|
|
39
|
+
writeBlockerResolution,
|
|
40
|
+
} from "../runtime/blocker-resolution.js";
|
|
41
|
+
|
|
42
|
+
interface McpRequest {
|
|
43
|
+
jsonrpc: "2.0";
|
|
44
|
+
id?: string | number;
|
|
45
|
+
method: string;
|
|
46
|
+
params?: any;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface McpResponse {
|
|
50
|
+
jsonrpc: "2.0";
|
|
51
|
+
id?: string | number | null;
|
|
52
|
+
result?: any;
|
|
53
|
+
error?: {
|
|
54
|
+
code: number;
|
|
55
|
+
message: string;
|
|
56
|
+
data?: any;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function formatMcpToolResult(value: unknown): {
|
|
61
|
+
content: Array<{ type: "text"; text: string }>;
|
|
62
|
+
} {
|
|
63
|
+
return {
|
|
64
|
+
content: [{
|
|
65
|
+
type: "text",
|
|
66
|
+
text: JSON.stringify(value ?? null),
|
|
67
|
+
}],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface Tool {
|
|
72
|
+
name: string;
|
|
73
|
+
description: string;
|
|
74
|
+
inputSchema: {
|
|
75
|
+
type: string;
|
|
76
|
+
properties: Record<string, any>;
|
|
77
|
+
required?: string[];
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface Resource {
|
|
82
|
+
uri: string;
|
|
83
|
+
name: string;
|
|
84
|
+
description?: string;
|
|
85
|
+
mimeType?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type McpHttpServer = {
|
|
89
|
+
origin: string;
|
|
90
|
+
close(): Promise<void>;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
interface Prompt {
|
|
94
|
+
name: string;
|
|
95
|
+
description?: string;
|
|
96
|
+
arguments?: Array<{
|
|
97
|
+
name: string;
|
|
98
|
+
description?: string;
|
|
99
|
+
required?: boolean;
|
|
100
|
+
}>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function taskClient(cfg: any): ControlPlaneClient {
|
|
104
|
+
const registryUrl = process.env.BUGMOLE_REGISTRY_URL ?? cfg.registry?.url ?? "https://api.bugmole.com";
|
|
105
|
+
const apiKey = process.env.BUGMOLE_API_KEY ?? cfg.registry?.api_key;
|
|
106
|
+
if (!apiKey) throw new Error("BUGMOLE_API_KEY is required for agent task queue operations");
|
|
107
|
+
const projectId = cfg.project?.id;
|
|
108
|
+
if (!projectId) throw new Error("A project id is required for agent task queue operations");
|
|
109
|
+
return new ControlPlaneClient({ registryUrl, apiKey, projectId });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function publishJourneyRevision(cfg: any, journeyId: string, status: "draft" | "published" = "draft") {
|
|
113
|
+
const apiKey = process.env.BUGMOLE_API_KEY ?? cfg.registry?.api_key;
|
|
114
|
+
if (!apiKey || !cfg.project?.id) return null;
|
|
115
|
+
const client = taskClient(cfg);
|
|
116
|
+
const journey = readJourneyGraph(cfg).journeys.find((item) => item.id === journeyId);
|
|
117
|
+
return client.saveJourneyRevision({
|
|
118
|
+
journeyId,
|
|
119
|
+
objectKey: `${projectPrefix(cfg.project.id)}/journeys/graph.json`,
|
|
120
|
+
expectedRevision: undefined,
|
|
121
|
+
appId: cfg.project.app_id ?? cfg.project.appId,
|
|
122
|
+
environmentId: cfg.project.environment_id ?? cfg.project.environmentId,
|
|
123
|
+
status,
|
|
124
|
+
sourceOfTruth: "canonical_flow",
|
|
125
|
+
stability: journey?.stability ?? "discovered",
|
|
126
|
+
linkedToGraph: journey?.linkedToGraph === true,
|
|
127
|
+
verificationRunId: journey?.verificationRunId,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function startMcpServer(cfg: any, options: { forceHttp?: boolean } = {}) {
|
|
132
|
+
// Handle boolean strings from env vars
|
|
133
|
+
const enabled =
|
|
134
|
+
cfg.mcp?.enabled === true || cfg.mcp?.enabled === "true" || cfg.mcp?.enabled === "1";
|
|
135
|
+
if (!enabled) {
|
|
136
|
+
console.log("[Bugmole MCP] MCP disabled");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check if running in stdio mode (for Cursor integration)
|
|
141
|
+
// When stdin is not a TTY, we're in stdio mode
|
|
142
|
+
const isStdioMode = !process.stdin.isTTY && !options.forceHttp;
|
|
143
|
+
|
|
144
|
+
if (isStdioMode) {
|
|
145
|
+
return startStdioServer(cfg);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const host = cfg.mcp.host || "127.0.0.1";
|
|
149
|
+
const basePort = cfg.mcp.port === undefined || cfg.mcp.port === null
|
|
150
|
+
? 3187
|
|
151
|
+
: Number(cfg.mcp.port);
|
|
152
|
+
|
|
153
|
+
// Try to find an available port, starting from the configured port
|
|
154
|
+
const server = http.createServer(async (req, res) => {
|
|
155
|
+
// CORS headers
|
|
156
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
157
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
158
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
159
|
+
|
|
160
|
+
if (req.method === "OPTIONS") {
|
|
161
|
+
res.writeHead(200);
|
|
162
|
+
res.end();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Health check
|
|
167
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
168
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
169
|
+
res.end(JSON.stringify({
|
|
170
|
+
ok: true,
|
|
171
|
+
name: cfg.name ?? "Bugmole MCP",
|
|
172
|
+
projectId: cfg.project?.id ?? "default",
|
|
173
|
+
baseUrl: cfg.execution?.playwright?.base_url,
|
|
174
|
+
}));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// MCP protocol endpoints
|
|
179
|
+
if (req.method === "POST" && req.url === "/mcp") {
|
|
180
|
+
await handleMcpRequest(req, res, cfg);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Initialize endpoint
|
|
185
|
+
if (req.method === "POST" && req.url === "/initialize") {
|
|
186
|
+
const response: McpResponse = {
|
|
187
|
+
jsonrpc: "2.0",
|
|
188
|
+
result: {
|
|
189
|
+
protocolVersion: "2024-11-05",
|
|
190
|
+
capabilities: {
|
|
191
|
+
tools: {},
|
|
192
|
+
resources: {},
|
|
193
|
+
prompts: {},
|
|
194
|
+
},
|
|
195
|
+
serverInfo: {
|
|
196
|
+
name: "bugmole",
|
|
197
|
+
version: "1.0.0",
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
202
|
+
res.end(JSON.stringify(response));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Tools list endpoint
|
|
207
|
+
if (req.method === "GET" && req.url === "/tools") {
|
|
208
|
+
const tools = getTools(cfg);
|
|
209
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
210
|
+
res.end(JSON.stringify({ tools }));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Resources list endpoint
|
|
215
|
+
if (req.method === "GET" && req.url === "/resources") {
|
|
216
|
+
const resources = getResources(cfg);
|
|
217
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
218
|
+
res.end(JSON.stringify({ resources }));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Prompts list endpoint
|
|
223
|
+
if (req.method === "GET" && req.url === "/prompts") {
|
|
224
|
+
const prompts = getPrompts(cfg);
|
|
225
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
226
|
+
res.end(JSON.stringify({ prompts }));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 404 for unknown routes
|
|
231
|
+
res.writeHead(404, { "content-type": "application/json" });
|
|
232
|
+
res.end(JSON.stringify({ error: "not_found" }));
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Try to listen on the configured port, or find an available port
|
|
236
|
+
let port = basePort;
|
|
237
|
+
const maxAttempts = 10;
|
|
238
|
+
|
|
239
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
240
|
+
try {
|
|
241
|
+
await new Promise<void>((resolve, reject) => {
|
|
242
|
+
const errorHandler = (err: NodeJS.ErrnoException) => {
|
|
243
|
+
server.removeListener("error", errorHandler);
|
|
244
|
+
if (err.code === "EADDRINUSE") {
|
|
245
|
+
// Port is in use, try next port
|
|
246
|
+
reject(err);
|
|
247
|
+
} else {
|
|
248
|
+
reject(err);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
server.once("error", errorHandler);
|
|
253
|
+
server.listen(port, host, () => {
|
|
254
|
+
server.removeListener("error", errorHandler);
|
|
255
|
+
resolve();
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Successfully bound to port
|
|
260
|
+
break;
|
|
261
|
+
} catch (error: any) {
|
|
262
|
+
if (error.code === "EADDRINUSE") {
|
|
263
|
+
if (attempt < maxAttempts - 1) {
|
|
264
|
+
// Try next port
|
|
265
|
+
port = basePort + attempt + 1;
|
|
266
|
+
// Close the server if it was created but failed to bind
|
|
267
|
+
try {
|
|
268
|
+
server.close();
|
|
269
|
+
} catch {
|
|
270
|
+
// Ignore errors when closing
|
|
271
|
+
}
|
|
272
|
+
continue;
|
|
273
|
+
} else {
|
|
274
|
+
// Last attempt failed, but don't crash - log and exit gracefully
|
|
275
|
+
console.error(
|
|
276
|
+
`[Bugmole MCP] Failed to start MCP server: could not find available port after ${maxAttempts} attempts (tried ${basePort}-${port})`,
|
|
277
|
+
);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
// Different error, throw it
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (port !== basePort) {
|
|
288
|
+
console.log(`[Bugmole MCP] Port ${basePort} was in use, using port ${port} instead`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const address = server.address();
|
|
292
|
+
const actualPort = address && typeof address !== "string" ? address.port : port;
|
|
293
|
+
console.log(`[Bugmole MCP] MCP server listening on http://${host}:${actualPort}`);
|
|
294
|
+
console.log(`[Bugmole MCP] MCP endpoints:`);
|
|
295
|
+
console.log(` POST /mcp - MCP protocol handler`);
|
|
296
|
+
console.log(` POST /initialize - Initialize MCP session`);
|
|
297
|
+
console.log(` GET /tools - List available tools`);
|
|
298
|
+
console.log(` GET /resources - List available resources`);
|
|
299
|
+
console.log(` GET /prompts - List available prompts`);
|
|
300
|
+
console.log(` GET /health - Health check`);
|
|
301
|
+
return {
|
|
302
|
+
origin: `http://${host}:${actualPort}`,
|
|
303
|
+
close: async () => {
|
|
304
|
+
await new Promise<void>((resolve, reject) => {
|
|
305
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
306
|
+
});
|
|
307
|
+
},
|
|
308
|
+
} satisfies McpHttpServer;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Start MCP server in stdio mode (for Cursor integration)
|
|
313
|
+
* Reads JSON-RPC requests from stdin and writes responses to stdout
|
|
314
|
+
*/
|
|
315
|
+
async function startStdioServer(cfg: any) {
|
|
316
|
+
// Suppress console.log and console.error in stdio mode to avoid polluting stdout
|
|
317
|
+
// All errors should go to stderr, and we'll restore console methods for critical errors
|
|
318
|
+
const originalLog = console.log;
|
|
319
|
+
const originalError = console.error;
|
|
320
|
+
const originalWarn = console.warn;
|
|
321
|
+
|
|
322
|
+
// Redirect all console output to stderr in stdio mode
|
|
323
|
+
// CRITICAL: Never write to stdout except for JSON-RPC responses
|
|
324
|
+
console.log = (...args: any[]) => {
|
|
325
|
+
process.stderr.write("[LOG] " + args.map(String).join(" ") + "\n");
|
|
326
|
+
};
|
|
327
|
+
console.error = (...args: any[]) => {
|
|
328
|
+
process.stderr.write("[ERROR] " + args.map(String).join(" ") + "\n");
|
|
329
|
+
};
|
|
330
|
+
console.warn = (...args: any[]) => {
|
|
331
|
+
process.stderr.write("[WARN] " + args.map(String).join(" ") + "\n");
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// Create readline interface - output should NOT go through readline
|
|
335
|
+
// We'll write directly to stdout for JSON-RPC responses
|
|
336
|
+
const rl = readline.createInterface({
|
|
337
|
+
input: process.stdin,
|
|
338
|
+
output: process.stderr, // Use stderr for readline output to avoid conflicts
|
|
339
|
+
terminal: false,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
rl.on("line", async (line: string) => {
|
|
343
|
+
if (!line.trim()) return;
|
|
344
|
+
|
|
345
|
+
// Skip lines that don't look like JSON (e.g., npm output)
|
|
346
|
+
if (!line.trim().startsWith("{") && !line.trim().startsWith("[")) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let request: McpRequest;
|
|
351
|
+
let requestId: string | number | null = null;
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
// Try to extract id first in case parsing fails
|
|
355
|
+
const parsed = JSON.parse(line);
|
|
356
|
+
requestId = parsed?.id ?? null;
|
|
357
|
+
request = parsed;
|
|
358
|
+
} catch (err) {
|
|
359
|
+
// For parse errors, we can't extract the id, so use null
|
|
360
|
+
// Only send error response if we have a valid JSON structure that failed to parse
|
|
361
|
+
// Ignore completely malformed input (like npm output)
|
|
362
|
+
if (line.trim().startsWith("{")) {
|
|
363
|
+
try {
|
|
364
|
+
// Try to extract id from malformed JSON as a last resort
|
|
365
|
+
const idMatch = line.match(/"id"\s*:\s*([^,}\]]+)/);
|
|
366
|
+
if (idMatch) {
|
|
367
|
+
const idValue = idMatch[1].trim().replace(/['"]/g, "");
|
|
368
|
+
requestId = isNaN(Number(idValue)) ? idValue : Number(idValue);
|
|
369
|
+
}
|
|
370
|
+
} catch {
|
|
371
|
+
// Ignore
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const response: McpResponse = {
|
|
375
|
+
jsonrpc: "2.0",
|
|
376
|
+
id: requestId,
|
|
377
|
+
error: { code: -32700, message: "Parse error" },
|
|
378
|
+
};
|
|
379
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Validate request structure
|
|
385
|
+
if (!request || typeof request !== "object") {
|
|
386
|
+
const response: McpResponse = {
|
|
387
|
+
jsonrpc: "2.0",
|
|
388
|
+
id: requestId,
|
|
389
|
+
error: { code: -32600, message: "Invalid Request" },
|
|
390
|
+
};
|
|
391
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (!request.method) {
|
|
396
|
+
const response: McpResponse = {
|
|
397
|
+
jsonrpc: "2.0",
|
|
398
|
+
id: request.id ?? requestId,
|
|
399
|
+
error: { code: -32600, message: "Invalid Request: method is required" },
|
|
400
|
+
};
|
|
401
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Handle notifications (requests without id) - don't send response
|
|
406
|
+
// In JSON-RPC 2.0, notifications don't have an id and don't get responses
|
|
407
|
+
if (request.id === undefined) {
|
|
408
|
+
// This is a notification, process it but don't send a response
|
|
409
|
+
try {
|
|
410
|
+
switch (request.method) {
|
|
411
|
+
case "notifications/initialized":
|
|
412
|
+
// MCP sends this after initialize - just acknowledge silently
|
|
413
|
+
break;
|
|
414
|
+
case "notifications/progress":
|
|
415
|
+
// Progress notifications - handle if needed
|
|
416
|
+
break;
|
|
417
|
+
default:
|
|
418
|
+
// Unknown notification, ignore silently
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
} catch {
|
|
422
|
+
// Ignore errors in notifications - never send response
|
|
423
|
+
}
|
|
424
|
+
return; // Don't send response for notifications
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// This is a request (has id), must send response
|
|
428
|
+
const response: McpResponse = {
|
|
429
|
+
jsonrpc: "2.0",
|
|
430
|
+
id: request.id,
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
switch (request.method) {
|
|
435
|
+
case "initialize":
|
|
436
|
+
response.result = {
|
|
437
|
+
protocolVersion: "2024-11-05",
|
|
438
|
+
capabilities: {
|
|
439
|
+
tools: {},
|
|
440
|
+
resources: {},
|
|
441
|
+
prompts: {},
|
|
442
|
+
},
|
|
443
|
+
serverInfo: {
|
|
444
|
+
name: "bugmole",
|
|
445
|
+
version: "1.0.0",
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
break;
|
|
449
|
+
|
|
450
|
+
case "tools/list":
|
|
451
|
+
response.result = { tools: getTools(cfg) };
|
|
452
|
+
break;
|
|
453
|
+
|
|
454
|
+
case "tools/call":
|
|
455
|
+
response.result = formatMcpToolResult(await callTool(request.params, cfg));
|
|
456
|
+
break;
|
|
457
|
+
|
|
458
|
+
case "resources/list":
|
|
459
|
+
response.result = { resources: getResources(cfg) };
|
|
460
|
+
break;
|
|
461
|
+
|
|
462
|
+
case "resources/read":
|
|
463
|
+
response.result = await readResource(request.params, cfg);
|
|
464
|
+
break;
|
|
465
|
+
|
|
466
|
+
case "prompts/list":
|
|
467
|
+
response.result = { prompts: getPrompts(cfg) };
|
|
468
|
+
break;
|
|
469
|
+
|
|
470
|
+
case "prompts/get":
|
|
471
|
+
response.result = await getPrompt(request.params, cfg);
|
|
472
|
+
break;
|
|
473
|
+
|
|
474
|
+
default:
|
|
475
|
+
response.error = {
|
|
476
|
+
code: -32601,
|
|
477
|
+
message: `Method not found: ${request.method}`,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
} catch (err: any) {
|
|
481
|
+
response.error = {
|
|
482
|
+
code: -32603,
|
|
483
|
+
message: err.message || "Internal error",
|
|
484
|
+
data: err.stack,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Write JSON-RPC response to stdout (must have id for requests)
|
|
489
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// Handle process errors gracefully
|
|
493
|
+
process.on("uncaughtException", (err) => {
|
|
494
|
+
originalError("[Bugmole MCP] Uncaught exception in stdio mode:", err);
|
|
495
|
+
process.exit(1);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
process.on("unhandledRejection", (reason) => {
|
|
499
|
+
originalError("[Bugmole MCP] Unhandled rejection in stdio mode:", reason);
|
|
500
|
+
process.exit(1);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Restore console methods (though they're redirected to stderr)
|
|
504
|
+
// This is mainly for cleanup
|
|
505
|
+
console.log = originalLog;
|
|
506
|
+
console.error = originalError;
|
|
507
|
+
console.warn = originalWarn;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function handleMcpRequest(req: http.IncomingMessage, res: http.ServerResponse, cfg: any) {
|
|
511
|
+
let body = "";
|
|
512
|
+
for await (const chunk of req) {
|
|
513
|
+
body += chunk.toString();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
let request: McpRequest;
|
|
517
|
+
try {
|
|
518
|
+
request = JSON.parse(body);
|
|
519
|
+
} catch (err) {
|
|
520
|
+
const response: McpResponse = {
|
|
521
|
+
jsonrpc: "2.0",
|
|
522
|
+
error: { code: -32700, message: "Parse error" },
|
|
523
|
+
};
|
|
524
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
525
|
+
res.end(JSON.stringify(response));
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (
|
|
530
|
+
!request ||
|
|
531
|
+
typeof request !== "object" ||
|
|
532
|
+
Array.isArray(request) ||
|
|
533
|
+
typeof request.method !== "string" ||
|
|
534
|
+
!request.method
|
|
535
|
+
) {
|
|
536
|
+
const response: McpResponse = {
|
|
537
|
+
jsonrpc: "2.0",
|
|
538
|
+
id: request && typeof request === "object" && !Array.isArray(request)
|
|
539
|
+
? request.id ?? null
|
|
540
|
+
: null,
|
|
541
|
+
error: { code: -32600, message: "Invalid Request" },
|
|
542
|
+
};
|
|
543
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
544
|
+
res.end(JSON.stringify(response));
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const response: McpResponse = {
|
|
549
|
+
jsonrpc: "2.0",
|
|
550
|
+
id: request.id,
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
switch (request.method) {
|
|
555
|
+
case "tools/list":
|
|
556
|
+
response.result = { tools: getTools(cfg) };
|
|
557
|
+
break;
|
|
558
|
+
|
|
559
|
+
case "tools/call":
|
|
560
|
+
response.result = formatMcpToolResult(await callTool(request.params, cfg));
|
|
561
|
+
break;
|
|
562
|
+
|
|
563
|
+
case "resources/list":
|
|
564
|
+
response.result = { resources: getResources(cfg) };
|
|
565
|
+
break;
|
|
566
|
+
|
|
567
|
+
case "resources/read":
|
|
568
|
+
response.result = await readResource(request.params, cfg);
|
|
569
|
+
break;
|
|
570
|
+
|
|
571
|
+
case "prompts/list":
|
|
572
|
+
response.result = { prompts: getPrompts(cfg) };
|
|
573
|
+
break;
|
|
574
|
+
|
|
575
|
+
case "prompts/get":
|
|
576
|
+
response.result = await getPrompt(request.params, cfg);
|
|
577
|
+
break;
|
|
578
|
+
|
|
579
|
+
default:
|
|
580
|
+
response.error = {
|
|
581
|
+
code: -32601,
|
|
582
|
+
message: `Method not found: ${request.method}`,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
} catch (err: any) {
|
|
586
|
+
response.error = {
|
|
587
|
+
code: -32603,
|
|
588
|
+
message: err.message || "Internal error",
|
|
589
|
+
data: err.stack,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
594
|
+
res.end(JSON.stringify(response));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export function getTools(cfg: any): Tool[] {
|
|
598
|
+
return [
|
|
599
|
+
{
|
|
600
|
+
name: "bugmole_environment_set",
|
|
601
|
+
description: "Record how a project environment is accessed and started without storing credentials.",
|
|
602
|
+
inputSchema: {
|
|
603
|
+
type: "object",
|
|
604
|
+
properties: {
|
|
605
|
+
environmentId: { type: "string" },
|
|
606
|
+
name: { type: "string" },
|
|
607
|
+
baseUrl: { type: "string" },
|
|
608
|
+
mockUrl: { type: "string" },
|
|
609
|
+
status: { type: "string", enum: ["active", "blocked", "planned"] },
|
|
610
|
+
access: {
|
|
611
|
+
type: "object",
|
|
612
|
+
properties: {
|
|
613
|
+
transport: { type: "string", enum: ["adb", "browser", "manual", "unknown"] },
|
|
614
|
+
deviceLabel: { type: "string" },
|
|
615
|
+
serialHint: { type: "string" },
|
|
616
|
+
appPackage: { type: "string" },
|
|
617
|
+
startCommand: { type: "string" },
|
|
618
|
+
notes: { type: "string" },
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
blockedReason: { type: "string" },
|
|
622
|
+
},
|
|
623
|
+
required: ["environmentId", "name", "access"],
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
name: "bugmole_environment_get",
|
|
628
|
+
description: "Read the non-secret access contract for a project environment before exploration.",
|
|
629
|
+
inputSchema: {
|
|
630
|
+
type: "object",
|
|
631
|
+
properties: { environmentId: { type: "string" } },
|
|
632
|
+
required: ["environmentId"],
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
name: "bugmole_ui_reference_register",
|
|
637
|
+
description: "Register a project-scoped UI mock/reference for parity and journey evidence.",
|
|
638
|
+
inputSchema: {
|
|
639
|
+
type: "object",
|
|
640
|
+
properties: {
|
|
641
|
+
kind: { type: "string", enum: ["image", "figma", "pdf", "url", "html", "existing_app"] },
|
|
642
|
+
location: { type: "string" },
|
|
643
|
+
viewport: { type: "object" },
|
|
644
|
+
checksum: { type: "string" },
|
|
645
|
+
relatedJourneyIds: { type: "array", items: { type: "string" } },
|
|
646
|
+
appId: { type: "string" },
|
|
647
|
+
environmentId: { type: "string" },
|
|
648
|
+
metadata: { type: "object" },
|
|
649
|
+
},
|
|
650
|
+
required: ["kind", "location"],
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
name: "bugmole_ui_reference_list",
|
|
655
|
+
description: "List project-scoped registered UI mocks/references.",
|
|
656
|
+
inputSchema: { type: "object", properties: {} },
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
name: "bugmole_ui_re_start",
|
|
660
|
+
description: "Start a resumable UI reverse-engineering coordinator job with a project-scoped reference input.",
|
|
661
|
+
inputSchema: {
|
|
662
|
+
type: "object",
|
|
663
|
+
properties: {
|
|
664
|
+
kind: { type: "string", enum: ["image", "figma", "pdf", "url", "html", "existing_app"] },
|
|
665
|
+
location: { type: "string", description: "Reference path, URL, Figma export, or current app location" },
|
|
666
|
+
viewport: { type: "object", description: "Optional reference viewport with width and height" }
|
|
667
|
+
},
|
|
668
|
+
required: ["kind", "location"]
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
name: "bugmole_ui_re_stage",
|
|
673
|
+
description: "Run one deterministic coordinator stage and persist its revisioned artifacts.",
|
|
674
|
+
inputSchema: {
|
|
675
|
+
type: "object",
|
|
676
|
+
properties: {
|
|
677
|
+
jobId: { type: "string" },
|
|
678
|
+
stage: { type: "string", enum: ["measure", "dna", "atoms", "reconcile", "complete"] },
|
|
679
|
+
mismatches: { type: "array", description: "Measured hierarchy or Design DNA mismatches to reconcile" }
|
|
680
|
+
},
|
|
681
|
+
required: ["jobId", "stage"]
|
|
682
|
+
}
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
name: "bugmole_ui_re_run",
|
|
686
|
+
description: "Run the deterministic Design DNA and Atomic Design coordinator pipeline for a job.",
|
|
687
|
+
inputSchema: {
|
|
688
|
+
type: "object",
|
|
689
|
+
properties: { jobId: { type: "string" } },
|
|
690
|
+
required: ["jobId"],
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
name: "bugmole_ui_re_status",
|
|
695
|
+
description: "Read a coordinator job and the current project Design DNA revision.",
|
|
696
|
+
inputSchema: {
|
|
697
|
+
type: "object",
|
|
698
|
+
properties: { jobId: { type: "string" } },
|
|
699
|
+
required: ["jobId"]
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
name: "bugmole_explore",
|
|
704
|
+
description:
|
|
705
|
+
"Start guided repository/application exploration. Inspect the project entrypoints, routes, specs, roles, and journey graph; after each phase check bugmole_journey_next and continue until the selected flow is exhausted or blocked.",
|
|
706
|
+
inputSchema: {
|
|
707
|
+
type: "object",
|
|
708
|
+
properties: {
|
|
709
|
+
baseUrl: { type: "string", description: "Optional application base URL; resolved from the environment contract when omitted" },
|
|
710
|
+
maxDepth: {
|
|
711
|
+
type: "number",
|
|
712
|
+
description: "Maximum exploration depth",
|
|
713
|
+
},
|
|
714
|
+
projectId: { type: "string" },
|
|
715
|
+
appId: { type: "string" },
|
|
716
|
+
environmentId: { type: "string" },
|
|
717
|
+
accessMethod: { type: "string", enum: ["configured", "handoff", "manual", "unknown"] },
|
|
718
|
+
journeyId: { type: "string", description: "Existing flow to continue checking after exploration" },
|
|
719
|
+
actor: { type: "string", description: "Actor for a newly discovered flow" },
|
|
720
|
+
},
|
|
721
|
+
required: [],
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
name: "bugmole_record_discovery",
|
|
726
|
+
description:
|
|
727
|
+
"Incrementally persist concrete authenticated screens, action transitions, button/link behavior, and terminal-goal completion observed by the agent/browser into a selected flow. Always follow the returned nextTask with bugmole_journey_next.",
|
|
728
|
+
inputSchema: {
|
|
729
|
+
type: "object",
|
|
730
|
+
properties: {
|
|
731
|
+
nodes: { type: "array", description: "Observed screen nodes with id, route, and signature" },
|
|
732
|
+
edges: { type: "array", description: "Observed action edges with from, to, action, and proof tokens" },
|
|
733
|
+
journeyId: { type: "string", description: "Named user journey to update" },
|
|
734
|
+
journeyName: { type: "string", description: "Human-readable journey name" },
|
|
735
|
+
actor: { type: "string", description: "Journey actor" },
|
|
736
|
+
nodeIds: { type: "array", items: { type: "string" }, description: "Optional ordered node IDs for this journey" },
|
|
737
|
+
edgeIds: { type: "array", items: { type: "string" }, description: "Optional ordered edge IDs for this journey" },
|
|
738
|
+
completedNodeIds: {
|
|
739
|
+
type: "array",
|
|
740
|
+
items: { type: "string" },
|
|
741
|
+
description: "Terminal goal screens whose user-facing action was completed and proven",
|
|
742
|
+
},
|
|
743
|
+
verifiedControlIds: {
|
|
744
|
+
type: "array",
|
|
745
|
+
items: { type: "string" },
|
|
746
|
+
description: "Buttons, links, and actions whose runtime behavior and destination were checked",
|
|
747
|
+
},
|
|
748
|
+
journeyRevision: { type: "number", description: "Optional expected revision for conflict-aware discovery updates" },
|
|
749
|
+
},
|
|
750
|
+
required: ["nodes", "edges"],
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
name: "bugmole_journey_list",
|
|
755
|
+
description: "Read the current project-scoped journey chart and all editable flows.",
|
|
756
|
+
inputSchema: { type: "object", properties: {} },
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
name: "bugmole_journey_get",
|
|
760
|
+
description: "Read one flow, its steps, transitions, revision, and next exploration guidance.",
|
|
761
|
+
inputSchema: {
|
|
762
|
+
type: "object",
|
|
763
|
+
properties: { journeyId: { type: "string" } },
|
|
764
|
+
required: ["journeyId"],
|
|
765
|
+
},
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
name: "bugmole_journey_create",
|
|
769
|
+
description: "Create a first-class editable user flow in the project journey chart.",
|
|
770
|
+
inputSchema: {
|
|
771
|
+
type: "object",
|
|
772
|
+
properties: {
|
|
773
|
+
id: { type: "string" },
|
|
774
|
+
name: { type: "string" },
|
|
775
|
+
description: { type: "string" },
|
|
776
|
+
actor: { type: "string" },
|
|
777
|
+
startNodeId: { type: "string" },
|
|
778
|
+
},
|
|
779
|
+
required: ["name", "actor"],
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
name: "bugmole_journey_add_step",
|
|
784
|
+
description: "Add an observed or planned step to an editable user flow.",
|
|
785
|
+
inputSchema: {
|
|
786
|
+
type: "object",
|
|
787
|
+
properties: {
|
|
788
|
+
journeyId: { type: "string" },
|
|
789
|
+
node: { type: "object" },
|
|
790
|
+
afterNodeId: { type: "string" },
|
|
791
|
+
},
|
|
792
|
+
required: ["journeyId", "node"],
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
name: "bugmole_journey_add_transition",
|
|
797
|
+
description: "Add a directed action/condition transition between flow steps.",
|
|
798
|
+
inputSchema: {
|
|
799
|
+
type: "object",
|
|
800
|
+
properties: {
|
|
801
|
+
journeyId: { type: "string" },
|
|
802
|
+
from: { type: "string" },
|
|
803
|
+
to: { type: "string" },
|
|
804
|
+
edge: { type: "object" },
|
|
805
|
+
},
|
|
806
|
+
required: ["journeyId", "from", "to"],
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
name: "bugmole_journey_update",
|
|
811
|
+
description: "Edit journey metadata, a step, or a transition and create a new draft revision.",
|
|
812
|
+
inputSchema: {
|
|
813
|
+
type: "object",
|
|
814
|
+
properties: {
|
|
815
|
+
journeyId: { type: "string" },
|
|
816
|
+
journey: { type: "object" },
|
|
817
|
+
node: { type: "object" },
|
|
818
|
+
edge: { type: "object" },
|
|
819
|
+
},
|
|
820
|
+
required: ["journeyId"],
|
|
821
|
+
},
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
name: "bugmole_journey_remove",
|
|
825
|
+
description: "Remove a step or transition from an editable flow.",
|
|
826
|
+
inputSchema: {
|
|
827
|
+
type: "object",
|
|
828
|
+
properties: { journeyId: { type: "string" }, nodeId: { type: "string" }, edgeId: { type: "string" } },
|
|
829
|
+
required: ["journeyId"],
|
|
830
|
+
},
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
name: "bugmole_journey_reuse_step",
|
|
834
|
+
description: "Reuse a verified step from another canonical flow without duplicating its implementation.",
|
|
835
|
+
inputSchema: {
|
|
836
|
+
type: "object",
|
|
837
|
+
properties: {
|
|
838
|
+
journeyId: { type: "string" },
|
|
839
|
+
sourceJourneyId: { type: "string" },
|
|
840
|
+
stepId: { type: "string" },
|
|
841
|
+
alias: { type: "string" },
|
|
842
|
+
},
|
|
843
|
+
required: ["journeyId", "sourceJourneyId", "stepId"],
|
|
844
|
+
},
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
name: "bugmole_journey_stabilize",
|
|
848
|
+
description: "Mark a canonical flow stable only after a successful verification run and link it into the app graph.",
|
|
849
|
+
inputSchema: {
|
|
850
|
+
type: "object",
|
|
851
|
+
properties: {
|
|
852
|
+
journeyId: { type: "string" },
|
|
853
|
+
verificationRunId: { type: "string" },
|
|
854
|
+
verifiedAt: { type: "string" },
|
|
855
|
+
},
|
|
856
|
+
required: ["journeyId", "verificationRunId"],
|
|
857
|
+
},
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
name: "bugmole_journey_next",
|
|
861
|
+
description: "Check the flow chart and return the next unexplored route, branch, or blocker.",
|
|
862
|
+
inputSchema: {
|
|
863
|
+
type: "object",
|
|
864
|
+
properties: { journeyId: { type: "string" } },
|
|
865
|
+
required: ["journeyId"],
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
name: "bugmole_resolve_blocker",
|
|
870
|
+
description:
|
|
871
|
+
"Record the user's clarification for a project-scoped exploration blocker and retry the selected journey.",
|
|
872
|
+
inputSchema: {
|
|
873
|
+
type: "object",
|
|
874
|
+
properties: {
|
|
875
|
+
journeyId: { type: "string" },
|
|
876
|
+
category: { type: "string" },
|
|
877
|
+
reason: { type: "string" },
|
|
878
|
+
route: { type: "string" },
|
|
879
|
+
nodeId: { type: "string" },
|
|
880
|
+
environmentId: { type: "string" },
|
|
881
|
+
question: { type: "string" },
|
|
882
|
+
resolution: { type: "string" },
|
|
883
|
+
expiresAt: { type: "string" },
|
|
884
|
+
},
|
|
885
|
+
required: ["journeyId", "category", "reason", "question", "resolution"],
|
|
886
|
+
},
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
name: "bugmole_plan",
|
|
890
|
+
description:
|
|
891
|
+
"Generate a workable journey plan only after guided exploration. Read bugmole://guidance/agent-testing and the roles, domain rules, blockers, and journey graph resources first; do not create a placeholder plan when discovery is incomplete.",
|
|
892
|
+
inputSchema: {
|
|
893
|
+
type: "object",
|
|
894
|
+
properties: {
|
|
895
|
+
journeyId: { type: "string", description: "Journey identifier" },
|
|
896
|
+
actor: { type: "string", description: "Role/actor for the journey" },
|
|
897
|
+
revision: { type: "number", description: "Optional immutable journey revision" },
|
|
898
|
+
},
|
|
899
|
+
required: ["journeyId", "actor"],
|
|
900
|
+
},
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
name: "bugmole_run",
|
|
904
|
+
description: "Execute a journey plan and collect evidence",
|
|
905
|
+
inputSchema: {
|
|
906
|
+
type: "object",
|
|
907
|
+
properties: {
|
|
908
|
+
planId: { type: "string", description: "Plan file ID or path" },
|
|
909
|
+
journeyId: {
|
|
910
|
+
type: "string",
|
|
911
|
+
description: "Specific journey ID to run",
|
|
912
|
+
},
|
|
913
|
+
},
|
|
914
|
+
required: ["planId"],
|
|
915
|
+
},
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
name: "bugmole_propose_diff",
|
|
919
|
+
description: "Propose a minimal diff to a spec file based on execution results",
|
|
920
|
+
inputSchema: {
|
|
921
|
+
type: "object",
|
|
922
|
+
properties: {
|
|
923
|
+
filePath: { type: "string", description: "Path to spec file" },
|
|
924
|
+
evidence: { type: "object", description: "Execution evidence" },
|
|
925
|
+
failures: { type: "array", description: "List of failures" },
|
|
926
|
+
},
|
|
927
|
+
required: ["filePath", "evidence"],
|
|
928
|
+
},
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
name: "bugmole_review",
|
|
932
|
+
description: "Review a proposed diff for safety and correctness",
|
|
933
|
+
inputSchema: {
|
|
934
|
+
type: "object",
|
|
935
|
+
properties: {
|
|
936
|
+
filePath: { type: "string", description: "Path to spec file" },
|
|
937
|
+
changeSummary: { type: "string", description: "Change summary" },
|
|
938
|
+
diff: { type: "string", description: "Proposed diff" },
|
|
939
|
+
evidence: { type: "object", description: "Supporting evidence" },
|
|
940
|
+
},
|
|
941
|
+
required: ["filePath", "changeSummary", "diff"],
|
|
942
|
+
},
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
name: "bugmole_report",
|
|
946
|
+
description: "Generate a health report with evidence links and plan diffs",
|
|
947
|
+
inputSchema: {
|
|
948
|
+
type: "object",
|
|
949
|
+
properties: {
|
|
950
|
+
runId: { type: "string", description: "Run identifier" },
|
|
951
|
+
format: {
|
|
952
|
+
type: "string",
|
|
953
|
+
enum: ["json", "markdown"],
|
|
954
|
+
description: "Report format",
|
|
955
|
+
},
|
|
956
|
+
},
|
|
957
|
+
},
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
name: "bugmole_write_test_cases",
|
|
961
|
+
description:
|
|
962
|
+
"Validate and write test case assumptions for a discovered journey. Every case must reference a node (and optionally an edge) that exists in that journey at that revision — assumptions are grounded in the real graph, never invented.",
|
|
963
|
+
inputSchema: {
|
|
964
|
+
type: "object",
|
|
965
|
+
properties: {
|
|
966
|
+
caseSet: {
|
|
967
|
+
type: "object",
|
|
968
|
+
description: "Case set object matching bugmole://spec/test-cases.schema.json",
|
|
969
|
+
},
|
|
970
|
+
},
|
|
971
|
+
required: ["caseSet"],
|
|
972
|
+
},
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
name: "bugmole_write_test_case_results",
|
|
976
|
+
description:
|
|
977
|
+
"Record what a run observed for test cases already written by bugmole_write_test_cases. Every result must name a caseId that exists in that case set at that revision. This is the only thing that moves a case off \"planned\" — assumptions never count as evidence.",
|
|
978
|
+
inputSchema: {
|
|
979
|
+
type: "object",
|
|
980
|
+
properties: {
|
|
981
|
+
resultSet: {
|
|
982
|
+
type: "object",
|
|
983
|
+
description: "Result set matching bugmole://spec/test-case-results.schema.json",
|
|
984
|
+
},
|
|
985
|
+
},
|
|
986
|
+
required: ["resultSet"],
|
|
987
|
+
},
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
name: "bugmole_write_ui_audit",
|
|
991
|
+
description:
|
|
992
|
+
"Validate and write a structured application UI audit JSON artifact for dashboard consumption",
|
|
993
|
+
inputSchema: {
|
|
994
|
+
type: "object",
|
|
995
|
+
properties: {
|
|
996
|
+
audit: {
|
|
997
|
+
type: "object",
|
|
998
|
+
description: "Audit object matching bugmole://spec/app-ui-audit.schema.json",
|
|
999
|
+
},
|
|
1000
|
+
},
|
|
1001
|
+
required: ["audit"],
|
|
1002
|
+
},
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
name: "bugmole_write_ui_parity_audit",
|
|
1006
|
+
description:
|
|
1007
|
+
"Validate and write a reference-versus-current UI parity audit JSON artifact for dashboard consumption",
|
|
1008
|
+
inputSchema: {
|
|
1009
|
+
type: "object",
|
|
1010
|
+
properties: {
|
|
1011
|
+
audit: {
|
|
1012
|
+
type: "object",
|
|
1013
|
+
description: "Audit object matching bugmole://spec/ui-parity-audit.schema.json",
|
|
1014
|
+
},
|
|
1015
|
+
},
|
|
1016
|
+
required: ["audit"],
|
|
1017
|
+
},
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
name: "bugmole_read_spec",
|
|
1021
|
+
description: "Read a spec file (roles, domain_rules, blockers, plans, etc.)",
|
|
1022
|
+
inputSchema: {
|
|
1023
|
+
type: "object",
|
|
1024
|
+
properties: {
|
|
1025
|
+
filePath: {
|
|
1026
|
+
type: "string",
|
|
1027
|
+
description: "Path to spec file relative to spec_dir",
|
|
1028
|
+
},
|
|
1029
|
+
},
|
|
1030
|
+
required: ["filePath"],
|
|
1031
|
+
},
|
|
1032
|
+
},
|
|
1033
|
+
{
|
|
1034
|
+
name: "bugmole_write_spec",
|
|
1035
|
+
description: "Write or update a spec file",
|
|
1036
|
+
inputSchema: {
|
|
1037
|
+
type: "object",
|
|
1038
|
+
properties: {
|
|
1039
|
+
filePath: {
|
|
1040
|
+
type: "string",
|
|
1041
|
+
description: "Path to spec file relative to spec_dir",
|
|
1042
|
+
},
|
|
1043
|
+
content: { type: "string", description: "File content" },
|
|
1044
|
+
},
|
|
1045
|
+
required: ["filePath", "content"],
|
|
1046
|
+
},
|
|
1047
|
+
},
|
|
1048
|
+
{
|
|
1049
|
+
name: "bugmole_report_progress",
|
|
1050
|
+
description:
|
|
1051
|
+
"Report progress on a long-running task. Use this periodically (every 2-3 minutes) to let the framework know you are still working. This prevents unnecessary nudges and allows the framework to track your progress. Only nudge if you have not reported progress for an extended period.",
|
|
1052
|
+
inputSchema: {
|
|
1053
|
+
type: "object",
|
|
1054
|
+
properties: {
|
|
1055
|
+
status: {
|
|
1056
|
+
type: "string",
|
|
1057
|
+
enum: ["working", "blocked", "completed"],
|
|
1058
|
+
description: "Current work status",
|
|
1059
|
+
},
|
|
1060
|
+
message: {
|
|
1061
|
+
type: "string",
|
|
1062
|
+
description: "Brief description of what you are currently working on",
|
|
1063
|
+
},
|
|
1064
|
+
details: {
|
|
1065
|
+
type: "string",
|
|
1066
|
+
description: "Optional additional details about your progress",
|
|
1067
|
+
},
|
|
1068
|
+
estimated_time_remaining: {
|
|
1069
|
+
type: "string",
|
|
1070
|
+
description: 'Optional estimate of time remaining (e.g., "5 minutes", "30 seconds")',
|
|
1071
|
+
},
|
|
1072
|
+
},
|
|
1073
|
+
required: ["status", "message"],
|
|
1074
|
+
},
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
name: "bugmole_task_next",
|
|
1078
|
+
description: "Claim the highest-priority queued coding task for this project and return its full task context.",
|
|
1079
|
+
inputSchema: {
|
|
1080
|
+
type: "object",
|
|
1081
|
+
properties: { workerId: { type: "string", description: "Stable coding-agent worker id" } },
|
|
1082
|
+
required: ["workerId"],
|
|
1083
|
+
},
|
|
1084
|
+
},
|
|
1085
|
+
{
|
|
1086
|
+
name: "bugmole_task_create",
|
|
1087
|
+
description:
|
|
1088
|
+
"Queue a follow-up agent task. Use this to hand the next stage of work to the queue rather than stopping. The idempotencyKey must describe the work so re-proposing the same follow-up updates it instead of creating a duplicate.",
|
|
1089
|
+
inputSchema: {
|
|
1090
|
+
type: "object",
|
|
1091
|
+
properties: {
|
|
1092
|
+
title: { type: "string" },
|
|
1093
|
+
description: { type: "string" },
|
|
1094
|
+
taskType: { type: "string", enum: ["code", "ui", "investigation"] },
|
|
1095
|
+
priority: { type: "number" },
|
|
1096
|
+
idempotencyKey: {
|
|
1097
|
+
type: "string",
|
|
1098
|
+
description: "Stable key for this unit of work, e.g. pipeline:<projectId>:<stage>:<journeyId>",
|
|
1099
|
+
},
|
|
1100
|
+
journeyId: { type: "string" },
|
|
1101
|
+
stepId: { type: "string" },
|
|
1102
|
+
},
|
|
1103
|
+
required: ["title", "description", "idempotencyKey"],
|
|
1104
|
+
},
|
|
1105
|
+
},
|
|
1106
|
+
{
|
|
1107
|
+
name: "bugmole_task_update",
|
|
1108
|
+
description: "Report progress, continuation steps, parity evidence, or a blocked/completed status for the claimed task.",
|
|
1109
|
+
inputSchema: {
|
|
1110
|
+
type: "object",
|
|
1111
|
+
properties: {
|
|
1112
|
+
taskId: { type: "string" },
|
|
1113
|
+
workerId: { type: "string" },
|
|
1114
|
+
status: { type: "string", enum: ["running", "blocked", "completed", "error"] },
|
|
1115
|
+
progressMessage: { type: "string" },
|
|
1116
|
+
continuation: { type: "object" },
|
|
1117
|
+
parityAuditId: { type: "string" },
|
|
1118
|
+
parityMatchScore: { type: "number" },
|
|
1119
|
+
completionSummary: { type: "string" },
|
|
1120
|
+
changedFiles: { type: "array", items: { type: "string" } },
|
|
1121
|
+
errorMessage: { type: "string" },
|
|
1122
|
+
},
|
|
1123
|
+
required: ["taskId", "workerId", "status"],
|
|
1124
|
+
},
|
|
1125
|
+
},
|
|
1126
|
+
{
|
|
1127
|
+
name: "bugmole_commit_response",
|
|
1128
|
+
description:
|
|
1129
|
+
"Finalize a Cursor task by committing structured response payload. This is the mandatory final step for all CURSOR_DRIVER tasks. Use the SIMPLEST possible JSON - only status and summary are required. Everything else is optional.",
|
|
1130
|
+
inputSchema: {
|
|
1131
|
+
type: "object",
|
|
1132
|
+
properties: {
|
|
1133
|
+
status: {
|
|
1134
|
+
type: "string",
|
|
1135
|
+
enum: ["ok", "error"],
|
|
1136
|
+
description: "Task completion status (REQUIRED)",
|
|
1137
|
+
},
|
|
1138
|
+
summary: {
|
|
1139
|
+
type: "string",
|
|
1140
|
+
description: "Short human-readable summary (REQUIRED)",
|
|
1141
|
+
},
|
|
1142
|
+
answer_markdown: {
|
|
1143
|
+
type: "string",
|
|
1144
|
+
description: "Optional: Full final answer in markdown format",
|
|
1145
|
+
},
|
|
1146
|
+
actions: {
|
|
1147
|
+
type: "array",
|
|
1148
|
+
description: "Optional: List of actions performed. Only include if you made changes.",
|
|
1149
|
+
items: {
|
|
1150
|
+
type: "object",
|
|
1151
|
+
properties: {
|
|
1152
|
+
type: {
|
|
1153
|
+
type: "string",
|
|
1154
|
+
enum: ["edit", "create", "delete", "command"],
|
|
1155
|
+
},
|
|
1156
|
+
target: { type: "string" },
|
|
1157
|
+
details: { type: "string" },
|
|
1158
|
+
},
|
|
1159
|
+
required: ["type"],
|
|
1160
|
+
},
|
|
1161
|
+
},
|
|
1162
|
+
artifacts: {
|
|
1163
|
+
type: "array",
|
|
1164
|
+
description: "Optional: Generated artifacts. Only include if you created artifacts.",
|
|
1165
|
+
items: {
|
|
1166
|
+
type: "object",
|
|
1167
|
+
properties: {
|
|
1168
|
+
type: {
|
|
1169
|
+
type: "string",
|
|
1170
|
+
enum: ["patch", "config", "command", "note"],
|
|
1171
|
+
},
|
|
1172
|
+
content: { type: "string" },
|
|
1173
|
+
},
|
|
1174
|
+
required: ["type"],
|
|
1175
|
+
},
|
|
1176
|
+
},
|
|
1177
|
+
next_steps: {
|
|
1178
|
+
type: "array",
|
|
1179
|
+
description:
|
|
1180
|
+
'CRITICAL: List of next steps as strings. REQUIRED if you mention "Next Steps", "TODO", or any future actions in your answer. Example: ["Run bugmole_explore with baseUrl=http://localhost:8000", "Create test plans"]. Only omit this field when EVERYTHING is 100% complete with NO remaining work.',
|
|
1181
|
+
items: {
|
|
1182
|
+
type: "string",
|
|
1183
|
+
},
|
|
1184
|
+
},
|
|
1185
|
+
},
|
|
1186
|
+
required: ["status", "summary"],
|
|
1187
|
+
},
|
|
1188
|
+
},
|
|
1189
|
+
];
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
export function getResources(cfg: any): Resource[] {
|
|
1193
|
+
const specDir = cfg.runtime.spec_dir || "./spec";
|
|
1194
|
+
const resources: Resource[] = [
|
|
1195
|
+
{
|
|
1196
|
+
uri: "bugmole://guidance/agent-testing",
|
|
1197
|
+
name: "Guided Agent Testing Workflow",
|
|
1198
|
+
description:
|
|
1199
|
+
"Persistent instructions for repository exploration, continuous MCP guidance, planning, and execution handoff",
|
|
1200
|
+
mimeType: "text/markdown",
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
uri: "bugmole://guidance/app-ui-audit",
|
|
1204
|
+
name: "Application UI Audit",
|
|
1205
|
+
description:
|
|
1206
|
+
"Guidance for reproducing a user flow and auditing every interactive control with runtime and source evidence",
|
|
1207
|
+
mimeType: "text/markdown",
|
|
1208
|
+
},
|
|
1209
|
+
{
|
|
1210
|
+
uri: "bugmole://spec/app-ui-audit.schema.json",
|
|
1211
|
+
name: "Application UI Audit Schema",
|
|
1212
|
+
description: "Stable JSON contract for dashboard-consumable UI audit artifacts",
|
|
1213
|
+
mimeType: "application/schema+json",
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
uri: "bugmole://spec/test-cases.schema.json",
|
|
1217
|
+
name: "Test Case Assumptions Schema",
|
|
1218
|
+
description: "Stable JSON contract for test case assumptions derived from a journey",
|
|
1219
|
+
mimeType: "application/schema+json",
|
|
1220
|
+
},
|
|
1221
|
+
{
|
|
1222
|
+
uri: "bugmole://spec/test-case-results.schema.json",
|
|
1223
|
+
name: "Test Case Results Schema",
|
|
1224
|
+
description: "Stable JSON contract for what a run observed for a case set — the only thing that moves a case off \"planned\"",
|
|
1225
|
+
mimeType: "application/schema+json",
|
|
1226
|
+
},
|
|
1227
|
+
{
|
|
1228
|
+
uri: "bugmole://guidance/ui-parity-audit",
|
|
1229
|
+
name: "UI Parity Audit",
|
|
1230
|
+
description:
|
|
1231
|
+
"Guidance for comparing a supplied reference experience with the implemented app and producing parity scores",
|
|
1232
|
+
mimeType: "text/markdown",
|
|
1233
|
+
},
|
|
1234
|
+
{
|
|
1235
|
+
uri: "bugmole://guidance/ui-reverse-engineering",
|
|
1236
|
+
name: "UI Reverse Engineering Coordinator",
|
|
1237
|
+
description: "Measurement-first Atomic Design and Design DNA coordinator workflow",
|
|
1238
|
+
mimeType: "text/markdown",
|
|
1239
|
+
},
|
|
1240
|
+
{
|
|
1241
|
+
uri: "bugmole://spec/ui-reverse-engineering.schema.json",
|
|
1242
|
+
name: "UI Reverse Engineering Schema",
|
|
1243
|
+
description: "Stable coordinator job, provenance, confidence, and Atomic Design contract",
|
|
1244
|
+
mimeType: "application/schema+json",
|
|
1245
|
+
},
|
|
1246
|
+
{
|
|
1247
|
+
uri: "bugmole://spec/design-dna.schema.json",
|
|
1248
|
+
name: "Design DNA Schema",
|
|
1249
|
+
description: "Project-wide reusable design token genome",
|
|
1250
|
+
mimeType: "application/schema+json",
|
|
1251
|
+
},
|
|
1252
|
+
{
|
|
1253
|
+
uri: "bugmole://guidance/agent-task-queue",
|
|
1254
|
+
name: "Agent Task Queue Workflow",
|
|
1255
|
+
description: "How a coding agent claims, executes, verifies, and completes dashboard-created tasks",
|
|
1256
|
+
mimeType: "text/markdown",
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
uri: "bugmole://spec/ui-parity-audit.schema.json",
|
|
1260
|
+
name: "UI Parity Audit Schema",
|
|
1261
|
+
description: "Stable JSON contract for dashboard-consumable reference/current parity results",
|
|
1262
|
+
mimeType: "application/schema+json",
|
|
1263
|
+
},
|
|
1264
|
+
{
|
|
1265
|
+
uri: "bugmole://spec/roles.yaml",
|
|
1266
|
+
name: "Roles and Capabilities",
|
|
1267
|
+
description: "Role and capability model",
|
|
1268
|
+
mimeType: "application/x-yaml",
|
|
1269
|
+
},
|
|
1270
|
+
{
|
|
1271
|
+
uri: "bugmole://spec/domain_rules.yaml",
|
|
1272
|
+
name: "Domain Rules",
|
|
1273
|
+
description: "Business prerequisites and invariants",
|
|
1274
|
+
mimeType: "application/x-yaml",
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
uri: "bugmole://spec/blockers.yaml",
|
|
1278
|
+
name: "Blocker Rules",
|
|
1279
|
+
description: "UI blocker handling rules",
|
|
1280
|
+
mimeType: "application/x-yaml",
|
|
1281
|
+
},
|
|
1282
|
+
{
|
|
1283
|
+
uri: "bugmole://spec/journeys.graph.json",
|
|
1284
|
+
name: "Journey Graph",
|
|
1285
|
+
description: "Journey graph (screens + actions)",
|
|
1286
|
+
mimeType: "application/json",
|
|
1287
|
+
},
|
|
1288
|
+
{
|
|
1289
|
+
uri: "bugmole://spec/bugs.index.json",
|
|
1290
|
+
name: "Bug Backlog",
|
|
1291
|
+
description: "Bug backlog index",
|
|
1292
|
+
mimeType: "application/json",
|
|
1293
|
+
},
|
|
1294
|
+
];
|
|
1295
|
+
|
|
1296
|
+
// Add plan files dynamically
|
|
1297
|
+
try {
|
|
1298
|
+
const plansDir = path.join(specDir, "plans");
|
|
1299
|
+
if (fs.existsSync(plansDir)) {
|
|
1300
|
+
const planFiles = fs.readdirSync(plansDir).filter((f) => f.endsWith(".flow.yaml"));
|
|
1301
|
+
for (const file of planFiles) {
|
|
1302
|
+
resources.push({
|
|
1303
|
+
uri: `bugmole://spec/plans/${file}`,
|
|
1304
|
+
name: `Plan: ${file.replace(".flow.yaml", "")}`,
|
|
1305
|
+
description: `Journey plan file: ${file}`,
|
|
1306
|
+
mimeType: "application/x-yaml",
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
} catch (err) {
|
|
1311
|
+
// Ignore errors reading plans directory
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
return resources;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
export function getPrompts(cfg: any): Prompt[] {
|
|
1318
|
+
return [
|
|
1319
|
+
{
|
|
1320
|
+
name: "guided_testing_workflow",
|
|
1321
|
+
description:
|
|
1322
|
+
"The required continuous-guidance loop for exploring a repository and producing a real executable QA plan",
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
name: "app_ui_audit",
|
|
1326
|
+
description:
|
|
1327
|
+
"Audit every interactive application control after reproducing a requested user flow",
|
|
1328
|
+
},
|
|
1329
|
+
{
|
|
1330
|
+
name: "ui_parity_audit",
|
|
1331
|
+
description:
|
|
1332
|
+
"Compare a supplied reference experience with the implemented application and produce scored parity results",
|
|
1333
|
+
},
|
|
1334
|
+
{
|
|
1335
|
+
name: "ui_reverse_engineering",
|
|
1336
|
+
description:
|
|
1337
|
+
"Orchestrate measurement-first Design DNA and Atomic Design reconstruction from reference inputs",
|
|
1338
|
+
},
|
|
1339
|
+
{
|
|
1340
|
+
name: "agent_task_execution",
|
|
1341
|
+
description: "Claim and complete the next dashboard-created coding task with required UI parity verification",
|
|
1342
|
+
},
|
|
1343
|
+
{
|
|
1344
|
+
name: "file_authoring",
|
|
1345
|
+
description: "Template for creating new spec files",
|
|
1346
|
+
arguments: [
|
|
1347
|
+
{
|
|
1348
|
+
name: "filePath",
|
|
1349
|
+
description: "Path to the file to create",
|
|
1350
|
+
required: true,
|
|
1351
|
+
},
|
|
1352
|
+
{
|
|
1353
|
+
name: "filePurpose",
|
|
1354
|
+
description: "Purpose of the file",
|
|
1355
|
+
required: true,
|
|
1356
|
+
},
|
|
1357
|
+
{ name: "domain", description: "Application domain", required: false },
|
|
1358
|
+
{
|
|
1359
|
+
name: "actors",
|
|
1360
|
+
description: "List of actors/roles",
|
|
1361
|
+
required: false,
|
|
1362
|
+
},
|
|
1363
|
+
],
|
|
1364
|
+
},
|
|
1365
|
+
{
|
|
1366
|
+
name: "diff_proposal",
|
|
1367
|
+
description: "Template for proposing spec file changes",
|
|
1368
|
+
arguments: [
|
|
1369
|
+
{
|
|
1370
|
+
name: "filePath",
|
|
1371
|
+
description: "Path to the file to modify",
|
|
1372
|
+
required: true,
|
|
1373
|
+
},
|
|
1374
|
+
{
|
|
1375
|
+
name: "filePurpose",
|
|
1376
|
+
description: "Purpose of the file",
|
|
1377
|
+
required: true,
|
|
1378
|
+
},
|
|
1379
|
+
{ name: "evidence", description: "Execution evidence", required: true },
|
|
1380
|
+
{ name: "failures", description: "List of failures", required: true },
|
|
1381
|
+
],
|
|
1382
|
+
},
|
|
1383
|
+
{
|
|
1384
|
+
name: "review_approval",
|
|
1385
|
+
description: "Template for reviewing change proposals",
|
|
1386
|
+
arguments: [
|
|
1387
|
+
{ name: "filePath", description: "Path to the file", required: true },
|
|
1388
|
+
{
|
|
1389
|
+
name: "filePurpose",
|
|
1390
|
+
description: "Purpose of the file",
|
|
1391
|
+
required: true,
|
|
1392
|
+
},
|
|
1393
|
+
{
|
|
1394
|
+
name: "changeSummary",
|
|
1395
|
+
description: "Proposed change summary",
|
|
1396
|
+
required: true,
|
|
1397
|
+
},
|
|
1398
|
+
{ name: "diff", description: "Proposed diff", required: true },
|
|
1399
|
+
{
|
|
1400
|
+
name: "evidence",
|
|
1401
|
+
description: "Supporting evidence",
|
|
1402
|
+
required: true,
|
|
1403
|
+
},
|
|
1404
|
+
],
|
|
1405
|
+
},
|
|
1406
|
+
];
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
export async function callTool(params: any, cfg: any): Promise<any> {
|
|
1410
|
+
const { name, arguments: args } = params;
|
|
1411
|
+
|
|
1412
|
+
// Add error handling wrapper to prevent server crashes
|
|
1413
|
+
try {
|
|
1414
|
+
return await callToolInternal(name, args, cfg);
|
|
1415
|
+
} catch (error: any) {
|
|
1416
|
+
// Log error to stderr (stdio mode redirects console to stderr)
|
|
1417
|
+
console.error(`[Bugmole MCP] Tool call error for ${name}:`, error.message);
|
|
1418
|
+
if (error.stack) {
|
|
1419
|
+
console.error(`[Bugmole MCP] Stack trace:`, error.stack);
|
|
1420
|
+
}
|
|
1421
|
+
// Re-throw so it gets properly formatted as JSON-RPC error
|
|
1422
|
+
throw error;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
export async function callToolInternal(name: string, args: any, cfg: any): Promise<any> {
|
|
1427
|
+
switch (name) {
|
|
1428
|
+
case "bugmole_ui_reference_register": {
|
|
1429
|
+
return {
|
|
1430
|
+
reference: await registerReferenceInput(cfg, {
|
|
1431
|
+
kind: args.kind,
|
|
1432
|
+
location: args.location,
|
|
1433
|
+
viewport: args.viewport,
|
|
1434
|
+
checksum: args.checksum,
|
|
1435
|
+
relatedJourneyIds: args.relatedJourneyIds,
|
|
1436
|
+
appId: args.appId,
|
|
1437
|
+
environmentId: args.environmentId,
|
|
1438
|
+
metadata: args.metadata,
|
|
1439
|
+
}),
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
case "bugmole_environment_set":
|
|
1444
|
+
return {
|
|
1445
|
+
environment: await setEnvironmentContract(cfg, {
|
|
1446
|
+
environmentId: args.environmentId,
|
|
1447
|
+
name: args.name,
|
|
1448
|
+
baseUrl: args.baseUrl,
|
|
1449
|
+
mockUrl: args.mockUrl,
|
|
1450
|
+
status: args.status,
|
|
1451
|
+
access: args.access,
|
|
1452
|
+
blockedReason: args.blockedReason,
|
|
1453
|
+
}),
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
case "bugmole_environment_get": {
|
|
1457
|
+
const environment = await getEnvironmentContract(cfg, args.environmentId);
|
|
1458
|
+
return environment
|
|
1459
|
+
? { environment }
|
|
1460
|
+
: { environment: null, message: "No environment access contract is registered yet." };
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
case "bugmole_ui_reference_list":
|
|
1464
|
+
return { references: await listReferenceInputs(cfg) };
|
|
1465
|
+
|
|
1466
|
+
case "bugmole_ui_re_start": {
|
|
1467
|
+
const input = normalizeReferenceInput({
|
|
1468
|
+
kind: args.kind,
|
|
1469
|
+
location: args.location,
|
|
1470
|
+
viewport: args.viewport,
|
|
1471
|
+
});
|
|
1472
|
+
return await startCoordinatorJob(cfg, input);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
case "bugmole_ui_re_stage": {
|
|
1476
|
+
if (typeof args.jobId !== "string") throw new Error("jobId is required");
|
|
1477
|
+
if (args.stage === "measure") return await measureReference(cfg, args.jobId);
|
|
1478
|
+
if (args.stage === "dna") return await inferDesignDNA(cfg, args.jobId);
|
|
1479
|
+
if (args.stage === "atoms") return await extractAtomicCandidates(cfg, args.jobId);
|
|
1480
|
+
if (args.stage === "reconcile") return await reconcileCoordinatorJob(cfg, args.jobId, Array.isArray(args.mismatches) ? args.mismatches : []);
|
|
1481
|
+
if (args.stage === "complete") return await completeCoordinatorJob(cfg, args.jobId);
|
|
1482
|
+
throw new Error(`Unsupported coordinator stage: ${args.stage}`);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
case "bugmole_ui_re_run":
|
|
1486
|
+
if (typeof args.jobId !== "string") throw new Error("jobId is required");
|
|
1487
|
+
return await runCoordinatorPipeline(cfg, args.jobId);
|
|
1488
|
+
|
|
1489
|
+
case "bugmole_ui_re_status": {
|
|
1490
|
+
if (typeof args.jobId !== "string") throw new Error("jobId is required");
|
|
1491
|
+
return {
|
|
1492
|
+
job: await getCoordinatorJob(cfg, args.jobId),
|
|
1493
|
+
designDNA: await getDesignDNA(cfg),
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
case "bugmole_explore": {
|
|
1498
|
+
const { explore } = await import("../runtime/explorer.js");
|
|
1499
|
+
return await explore(cfg, {
|
|
1500
|
+
baseUrl: args.baseUrl,
|
|
1501
|
+
maxDepth: args.maxDepth,
|
|
1502
|
+
projectId: args.projectId,
|
|
1503
|
+
appId: args.appId,
|
|
1504
|
+
environmentId: args.environmentId,
|
|
1505
|
+
accessMethod: args.accessMethod,
|
|
1506
|
+
journeyId: args.journeyId,
|
|
1507
|
+
actor: args.actor,
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
case "bugmole_journey_list": {
|
|
1512
|
+
const graph = readJourneyGraph(cfg);
|
|
1513
|
+
return {
|
|
1514
|
+
graph,
|
|
1515
|
+
journeys: graph.journeys,
|
|
1516
|
+
nextSteps: graph.journeys.length
|
|
1517
|
+
? [`Call bugmole_journey_next for ${graph.journeys[0].id}`]
|
|
1518
|
+
: ["Call bugmole_journey_create to create an editable flow"],
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
case "bugmole_journey_get": {
|
|
1523
|
+
const graph = readJourneyGraph(cfg);
|
|
1524
|
+
const journey = graph.journeys.find((item) => item.id === args.journeyId);
|
|
1525
|
+
if (!journey) throw new Error(`Journey not found: ${args.journeyId}`);
|
|
1526
|
+
return {
|
|
1527
|
+
journey,
|
|
1528
|
+
nodes: graph.nodes.filter((node) => journey.nodeIds.includes(node.id)),
|
|
1529
|
+
edges: graph.edges.filter((edge) => journey.edgeIds.includes(edge.id)),
|
|
1530
|
+
nextTask: nextJourneyTask(cfg, args.journeyId),
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
case "bugmole_journey_create": {
|
|
1535
|
+
const result = await createJourney(cfg, {
|
|
1536
|
+
id: args.id,
|
|
1537
|
+
name: args.name,
|
|
1538
|
+
description: args.description,
|
|
1539
|
+
actor: args.actor,
|
|
1540
|
+
startNodeId: args.startNodeId,
|
|
1541
|
+
});
|
|
1542
|
+
const revision = await publishJourneyRevision(cfg, result.journey.id);
|
|
1543
|
+
return { status: "success", ...result, revision, nextTask: nextJourneyTask(cfg, result.journey.id) };
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
case "bugmole_journey_add_step": {
|
|
1547
|
+
const result = await addJourneyStep(cfg, {
|
|
1548
|
+
journeyId: args.journeyId,
|
|
1549
|
+
node: args.node,
|
|
1550
|
+
afterNodeId: args.afterNodeId,
|
|
1551
|
+
});
|
|
1552
|
+
const revision = await publishJourneyRevision(cfg, result.journey.id);
|
|
1553
|
+
return { status: "success", ...result, revision, nextTask: nextJourneyTask(cfg, result.journey.id) };
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
case "bugmole_journey_add_transition": {
|
|
1557
|
+
const result = await addJourneyTransition(cfg, {
|
|
1558
|
+
journeyId: args.journeyId,
|
|
1559
|
+
from: args.from,
|
|
1560
|
+
to: args.to,
|
|
1561
|
+
edge: args.edge,
|
|
1562
|
+
});
|
|
1563
|
+
const revision = await publishJourneyRevision(cfg, result.journey.id);
|
|
1564
|
+
return { status: "success", ...result, revision, nextTask: nextJourneyTask(cfg, result.journey.id) };
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
case "bugmole_journey_update": {
|
|
1568
|
+
const result = await updateJourney(cfg, {
|
|
1569
|
+
journeyId: args.journeyId,
|
|
1570
|
+
journey: args.journey,
|
|
1571
|
+
node: args.node,
|
|
1572
|
+
edge: args.edge,
|
|
1573
|
+
});
|
|
1574
|
+
const revision = await publishJourneyRevision(cfg, result.journey.id);
|
|
1575
|
+
return { status: "success", ...result, revision, nextTask: nextJourneyTask(cfg, result.journey.id) };
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
case "bugmole_journey_remove": {
|
|
1579
|
+
const result = await removeJourneyItem(cfg, {
|
|
1580
|
+
journeyId: args.journeyId,
|
|
1581
|
+
nodeId: args.nodeId,
|
|
1582
|
+
edgeId: args.edgeId,
|
|
1583
|
+
});
|
|
1584
|
+
const revision = await publishJourneyRevision(cfg, result.journey.id);
|
|
1585
|
+
return { status: "success", ...result, revision, nextTask: nextJourneyTask(cfg, result.journey.id) };
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
case "bugmole_journey_reuse_step": {
|
|
1589
|
+
const result = await reuseJourneyStep(cfg, {
|
|
1590
|
+
journeyId: args.journeyId,
|
|
1591
|
+
sourceJourneyId: args.sourceJourneyId,
|
|
1592
|
+
stepId: args.stepId,
|
|
1593
|
+
alias: args.alias,
|
|
1594
|
+
});
|
|
1595
|
+
const revision = await publishJourneyRevision(cfg, result.journey.id);
|
|
1596
|
+
return { status: "success", ...result, revision, nextTask: nextJourneyTask(cfg, result.journey.id) };
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
case "bugmole_journey_stabilize": {
|
|
1600
|
+
const result = await stabilizeJourney(cfg, {
|
|
1601
|
+
journeyId: args.journeyId,
|
|
1602
|
+
verificationRunId: args.verificationRunId,
|
|
1603
|
+
verifiedAt: args.verifiedAt,
|
|
1604
|
+
});
|
|
1605
|
+
const revision = await publishJourneyRevision(cfg, result.journey.id, "published");
|
|
1606
|
+
return { status: "success", ...result, revision };
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
case "bugmole_journey_next": {
|
|
1610
|
+
return nextJourneyTaskWithClarification(cfg, args.journeyId);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
case "bugmole_resolve_blocker": {
|
|
1614
|
+
const environmentId = args.environmentId ??
|
|
1615
|
+
cfg.project?.environment_id ??
|
|
1616
|
+
cfg.project?.environmentId;
|
|
1617
|
+
const resolution = await writeBlockerResolution(cfg, {
|
|
1618
|
+
category: args.category,
|
|
1619
|
+
reason: args.reason,
|
|
1620
|
+
route: args.route,
|
|
1621
|
+
nodeId: args.nodeId,
|
|
1622
|
+
environmentId,
|
|
1623
|
+
question: args.question,
|
|
1624
|
+
resolution: args.resolution,
|
|
1625
|
+
expiresAt: args.expiresAt,
|
|
1626
|
+
});
|
|
1627
|
+
const graph = readJourneyGraph(cfg);
|
|
1628
|
+
const node = graph.nodes.find((item) =>
|
|
1629
|
+
(args.nodeId && item.id === args.nodeId) ||
|
|
1630
|
+
(args.route && item.route === args.route),
|
|
1631
|
+
);
|
|
1632
|
+
if (node) {
|
|
1633
|
+
const runtimeProbe = (node as any).runtimeProbe ?? {};
|
|
1634
|
+
(node as any).runtimeProbe = {
|
|
1635
|
+
...runtimeProbe,
|
|
1636
|
+
status: "resolved",
|
|
1637
|
+
resolutionId: resolution.resolutionId,
|
|
1638
|
+
resolvedAt: resolution.updatedAt,
|
|
1639
|
+
};
|
|
1640
|
+
await writeJourneyGraph(cfg, graph);
|
|
1641
|
+
await publishJourneyRevision(cfg, args.journeyId);
|
|
1642
|
+
}
|
|
1643
|
+
return {
|
|
1644
|
+
status: "success",
|
|
1645
|
+
resolution,
|
|
1646
|
+
journeyId: args.journeyId,
|
|
1647
|
+
nextTask: nextJourneyTask(cfg, args.journeyId),
|
|
1648
|
+
nextSteps: [
|
|
1649
|
+
"Verify the resolved blocker evidence",
|
|
1650
|
+
`Call bugmole_journey_next with journeyId=${args.journeyId}`,
|
|
1651
|
+
],
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
case "bugmole_record_discovery": {
|
|
1656
|
+
const specDir = cfg.runtime.spec_dir || "./spec";
|
|
1657
|
+
const graphPath = path.join(specDir, "journeys.graph.json");
|
|
1658
|
+
const graph = fs.existsSync(graphPath)
|
|
1659
|
+
? JSON.parse(fs.readFileSync(graphPath, "utf8"))
|
|
1660
|
+
: { version: 2, journeys: [], nodes: [], edges: [] };
|
|
1661
|
+
const nodes = Array.isArray(args.nodes) ? args.nodes : [];
|
|
1662
|
+
const edges = Array.isArray(args.edges) ? args.edges : [];
|
|
1663
|
+
const nodeMap = new Map((graph.nodes ?? []).map((node: any) => [node.id, node]));
|
|
1664
|
+
const edgeMap = new Map((graph.edges ?? []).map((edge: any, index: number) => [edge.id || `edge_${index}`, edge]));
|
|
1665
|
+
for (const node of nodes) {
|
|
1666
|
+
if (typeof node?.id === "string" && typeof node?.route === "string") nodeMap.set(node.id, node);
|
|
1667
|
+
}
|
|
1668
|
+
for (const edge of edges) {
|
|
1669
|
+
if (typeof edge?.from === "string" && typeof edge?.to === "string" && typeof edge?.action === "string") {
|
|
1670
|
+
const edgeId = edge.id || `${edge.from}->${edge.to}:${edge.action}`;
|
|
1671
|
+
edgeMap.set(edgeId, { ...edge, id: edgeId });
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
graph.nodes = [...nodeMap.values()];
|
|
1675
|
+
graph.edges = [...edgeMap.values()];
|
|
1676
|
+
graph.version = 2;
|
|
1677
|
+
const journeyId = typeof args.journeyId === "string" && args.journeyId.trim()
|
|
1678
|
+
? args.journeyId.trim()
|
|
1679
|
+
: "discovered-flow";
|
|
1680
|
+
const existingJourney = Array.isArray(graph.journeys)
|
|
1681
|
+
? graph.journeys.find((journey: any) => journey.id === journeyId)
|
|
1682
|
+
: undefined;
|
|
1683
|
+
const selectedNodeIds = Array.isArray(args.nodeIds) && args.nodeIds.length
|
|
1684
|
+
? args.nodeIds.filter((nodeId: unknown) => nodeMap.has(nodeId))
|
|
1685
|
+
: [
|
|
1686
|
+
...(Array.isArray(existingJourney?.nodeIds) ? existingJourney.nodeIds : []),
|
|
1687
|
+
...nodes.map((node: any) => node.id),
|
|
1688
|
+
];
|
|
1689
|
+
const selectedEdgeIds = Array.isArray(args.edgeIds) && args.edgeIds.length
|
|
1690
|
+
? args.edgeIds.filter((edgeId: unknown) => edgeMap.has(edgeId))
|
|
1691
|
+
: [
|
|
1692
|
+
...(Array.isArray(existingJourney?.edgeIds) ? existingJourney.edgeIds : []),
|
|
1693
|
+
...edges.map((edge: any) => edge.id || `${edge.from}->${edge.to}:${edge.action}`),
|
|
1694
|
+
];
|
|
1695
|
+
const completedNodeIds = Array.isArray(args.completedNodeIds)
|
|
1696
|
+
? args.completedNodeIds.filter((nodeId: unknown): nodeId is string => typeof nodeId === "string" && nodeMap.has(nodeId))
|
|
1697
|
+
: [];
|
|
1698
|
+
const verifiedControlIds = Array.isArray(args.verifiedControlIds)
|
|
1699
|
+
? args.verifiedControlIds.filter((controlId: unknown): controlId is string => typeof controlId === "string")
|
|
1700
|
+
: [];
|
|
1701
|
+
const existingMetadata = existingJourney?.metadata ?? {};
|
|
1702
|
+
const observedNodeIds = nodes
|
|
1703
|
+
.map((node: any) => node?.id)
|
|
1704
|
+
.filter((nodeId: unknown): nodeId is string => typeof nodeId === "string" && nodeMap.has(nodeId));
|
|
1705
|
+
const observedEdgeIds = edges
|
|
1706
|
+
.map((edge: any, index: number) => edge?.id || `${edge?.from}->${edge?.to}:${edge?.action ?? index}`)
|
|
1707
|
+
.filter((edgeId: unknown): edgeId is string => typeof edgeId === "string" && edgeMap.has(edgeId));
|
|
1708
|
+
const verificationMetadata = existingMetadata.flowScope === "path"
|
|
1709
|
+
? {
|
|
1710
|
+
verificationStatus: completedNodeIds.length ? "completed" : "in-progress",
|
|
1711
|
+
verifiedNodeIds: [...new Set([
|
|
1712
|
+
...(Array.isArray(existingMetadata.verifiedNodeIds) ? existingMetadata.verifiedNodeIds : []),
|
|
1713
|
+
...observedNodeIds,
|
|
1714
|
+
])],
|
|
1715
|
+
verifiedEdgeIds: [...new Set([
|
|
1716
|
+
...(Array.isArray(existingMetadata.verifiedEdgeIds) ? existingMetadata.verifiedEdgeIds : []),
|
|
1717
|
+
...observedEdgeIds,
|
|
1718
|
+
])],
|
|
1719
|
+
completedGoalIds: [...new Set([
|
|
1720
|
+
...(Array.isArray(existingMetadata.completedGoalIds) ? existingMetadata.completedGoalIds : []),
|
|
1721
|
+
...completedNodeIds,
|
|
1722
|
+
])],
|
|
1723
|
+
verifiedControlIds: [...new Set([
|
|
1724
|
+
...(Array.isArray(existingMetadata.verifiedControlIds) ? existingMetadata.verifiedControlIds : []),
|
|
1725
|
+
...verifiedControlIds,
|
|
1726
|
+
])],
|
|
1727
|
+
}
|
|
1728
|
+
: {};
|
|
1729
|
+
graph.journeys = [
|
|
1730
|
+
...(Array.isArray(graph.journeys) ? graph.journeys.filter((journey: any) => journey.id !== journeyId) : []),
|
|
1731
|
+
{
|
|
1732
|
+
id: journeyId,
|
|
1733
|
+
name: typeof args.journeyName === "string" && args.journeyName.trim()
|
|
1734
|
+
? args.journeyName.trim()
|
|
1735
|
+
: existingJourney?.name ?? "Discovered user flow",
|
|
1736
|
+
description: existingJourney?.description ?? "Flow assembled from exploration and discovery.",
|
|
1737
|
+
actor: typeof args.actor === "string" && args.actor.trim() ? args.actor.trim() : existingJourney?.actor ?? "user",
|
|
1738
|
+
startNodeId: (selectedNodeIds[0] ?? graph.nodes[0]?.id),
|
|
1739
|
+
nodeIds: [...new Set(selectedNodeIds)],
|
|
1740
|
+
edgeIds: [...new Set(selectedEdgeIds)],
|
|
1741
|
+
revision: (existingJourney?.revision ?? 0) + 1,
|
|
1742
|
+
status: "draft",
|
|
1743
|
+
// This path only ever handles exploration-discovered content
|
|
1744
|
+
// (never the manual journey-editor tools), so it's safe to always
|
|
1745
|
+
// mark it canonical/stable — see explorer.ts's matching comment.
|
|
1746
|
+
sourceOfTruth: "canonical_flow",
|
|
1747
|
+
stability: "stable",
|
|
1748
|
+
linkedToGraph: true,
|
|
1749
|
+
metadata: {
|
|
1750
|
+
...existingMetadata,
|
|
1751
|
+
...verificationMetadata,
|
|
1752
|
+
},
|
|
1753
|
+
},
|
|
1754
|
+
];
|
|
1755
|
+
graph.metadata = {
|
|
1756
|
+
...(graph.metadata ?? {}),
|
|
1757
|
+
lastAuthenticatedDiscoveryAt: new Date().toISOString(),
|
|
1758
|
+
authenticatedDiscovery: true,
|
|
1759
|
+
};
|
|
1760
|
+
fs.mkdirSync(specDir, { recursive: true });
|
|
1761
|
+
fs.writeFileSync(graphPath, JSON.stringify(graph, null, 2) + "\n", "utf8");
|
|
1762
|
+
const store = createProjectObjectStore(cfg);
|
|
1763
|
+
await store.put(
|
|
1764
|
+
`${projectPrefix(cfg.project?.id ?? "default")}/journeys/graph.json`,
|
|
1765
|
+
JSON.stringify(graph, null, 2) + "\n",
|
|
1766
|
+
"application/json",
|
|
1767
|
+
);
|
|
1768
|
+
return {
|
|
1769
|
+
status: "success",
|
|
1770
|
+
screensRecorded: nodes.length,
|
|
1771
|
+
actionsRecorded: edges.length,
|
|
1772
|
+
journeyId,
|
|
1773
|
+
journeyRevision: existingJourney?.revision ? existingJourney.revision + 1 : 1,
|
|
1774
|
+
nextTask: nextJourneyTask(cfg, journeyId),
|
|
1775
|
+
nextSteps: [
|
|
1776
|
+
"Read bugmole://spec/journeys.graph.json and inspect the updated flow",
|
|
1777
|
+
`Call bugmole_journey_next with journeyId=${journeyId}`,
|
|
1778
|
+
"Perform and record the returned next task before planning",
|
|
1779
|
+
],
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
case "bugmole_plan": {
|
|
1784
|
+
const { plan } = await import("../runtime/planner.js");
|
|
1785
|
+
return await plan(cfg, {
|
|
1786
|
+
journeyId: args.journeyId,
|
|
1787
|
+
actor: args.actor,
|
|
1788
|
+
revision: args.revision,
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
case "bugmole_run": {
|
|
1793
|
+
const { execute } = await import("../runtime/executor.js");
|
|
1794
|
+
return await execute(cfg, {
|
|
1795
|
+
planId: args.planId,
|
|
1796
|
+
journeyId: args.journeyId,
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
case "bugmole_propose_diff": {
|
|
1801
|
+
const { proposeDiff } = await import("../runtime/diff-proposal.js");
|
|
1802
|
+
return await proposeDiff(cfg, {
|
|
1803
|
+
filePath: args.filePath,
|
|
1804
|
+
evidence: args.evidence,
|
|
1805
|
+
failures: args.failures,
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
case "bugmole_review": {
|
|
1810
|
+
const { review } = await import("../runtime/reviewer.js");
|
|
1811
|
+
return await review(cfg, {
|
|
1812
|
+
filePath: args.filePath,
|
|
1813
|
+
changeSummary: args.changeSummary,
|
|
1814
|
+
diff: args.diff,
|
|
1815
|
+
evidence: args.evidence,
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
case "bugmole_report": {
|
|
1820
|
+
const { generateReport } = await import("../runtime/reporter.js");
|
|
1821
|
+
return await generateReport(cfg, {
|
|
1822
|
+
runId: args.runId,
|
|
1823
|
+
format: args.format,
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
case "bugmole_write_test_cases": {
|
|
1828
|
+
const caseSet = args.caseSet;
|
|
1829
|
+
if (!caseSet || typeof caseSet !== "object" || Array.isArray(caseSet)) {
|
|
1830
|
+
throw new Error("caseSet must be an object matching test-cases.schema.json");
|
|
1831
|
+
}
|
|
1832
|
+
if (caseSet.schemaVersion !== 1 || typeof caseSet.caseSetId !== "string" || !caseSet.caseSetId) {
|
|
1833
|
+
throw new Error("caseSet.schemaVersion must be 1 and caseSet.caseSetId is required");
|
|
1834
|
+
}
|
|
1835
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(caseSet.caseSetId)) {
|
|
1836
|
+
throw new Error("caseSet.caseSetId may contain only letters, numbers, ., _, and -");
|
|
1837
|
+
}
|
|
1838
|
+
if (typeof caseSet.journeyId !== "string" || !Number.isInteger(caseSet.journeyRevision)) {
|
|
1839
|
+
throw new Error("caseSet requires journeyId and an integer journeyRevision");
|
|
1840
|
+
}
|
|
1841
|
+
if (!Array.isArray(caseSet.cases) || caseSet.cases.length === 0) {
|
|
1842
|
+
throw new Error("caseSet.cases must be a non-empty array");
|
|
1843
|
+
}
|
|
1844
|
+
// Ground every assumption in the real graph. Without this the tool
|
|
1845
|
+
// would happily persist confident-sounding cases for steps that do
|
|
1846
|
+
// not exist — the same failure mode as inventing routes.
|
|
1847
|
+
const graph = readJourneyGraph(cfg);
|
|
1848
|
+
const journey = graph.journeys.find((item) => item.id === caseSet.journeyId);
|
|
1849
|
+
if (!journey) throw new Error(`Journey not found: ${caseSet.journeyId}`);
|
|
1850
|
+
if (journey.revision !== caseSet.journeyRevision) {
|
|
1851
|
+
throw new Error(
|
|
1852
|
+
`caseSet.journeyRevision must match the selected journey revision (${journey.revision})`,
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
const allowedNodes = new Set(journey.nodeIds);
|
|
1856
|
+
const allowedEdges = new Set(journey.edgeIds);
|
|
1857
|
+
const seenCaseIds = new Set<string>();
|
|
1858
|
+
for (const testCase of caseSet.cases) {
|
|
1859
|
+
if (!testCase || typeof testCase !== "object") throw new Error("each case must be an object");
|
|
1860
|
+
for (const field of ["caseId", "title", "nodeId", "intent"]) {
|
|
1861
|
+
if (typeof testCase[field] !== "string" || !testCase[field].trim()) {
|
|
1862
|
+
throw new Error(`each case requires a non-empty ${field}`);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(testCase.caseId)) {
|
|
1866
|
+
throw new Error(`case ${testCase.caseId}: caseId may contain only letters, numbers, ., _, and -`);
|
|
1867
|
+
}
|
|
1868
|
+
if (seenCaseIds.has(testCase.caseId)) throw new Error(`duplicate caseId: ${testCase.caseId}`);
|
|
1869
|
+
seenCaseIds.add(testCase.caseId);
|
|
1870
|
+
if (!allowedNodes.has(testCase.nodeId)) {
|
|
1871
|
+
throw new Error(
|
|
1872
|
+
`case ${testCase.caseId}: nodeId "${testCase.nodeId}" is not a step of journey ${caseSet.journeyId}`,
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1875
|
+
if (testCase.edgeId !== undefined && !allowedEdges.has(testCase.edgeId)) {
|
|
1876
|
+
throw new Error(
|
|
1877
|
+
`case ${testCase.caseId}: edgeId "${testCase.edgeId}" is not a transition of journey ${caseSet.journeyId}`,
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
for (const field of ["steps", "expected"]) {
|
|
1881
|
+
const value = testCase[field];
|
|
1882
|
+
if (!Array.isArray(value) || value.length === 0
|
|
1883
|
+
|| value.some((item: unknown) => typeof item !== "string" || !item.trim())) {
|
|
1884
|
+
throw new Error(`case ${testCase.caseId}: ${field} must be a non-empty array of strings`);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
if (!["high", "medium", "low"].includes(testCase.confidence)) {
|
|
1888
|
+
throw new Error(`case ${testCase.caseId}: confidence must be high, medium, or low`);
|
|
1889
|
+
}
|
|
1890
|
+
// An assumption with nothing behind it must say so rather than
|
|
1891
|
+
// claiming certainty it hasn't earned.
|
|
1892
|
+
const basis = Array.isArray(testCase.basis) ? testCase.basis.filter(Boolean) : [];
|
|
1893
|
+
if (basis.length === 0 && testCase.confidence !== "low") {
|
|
1894
|
+
throw new Error(
|
|
1895
|
+
`case ${testCase.caseId}: a case with no basis must be confidence "low"`,
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
const artifactsDir = cfg.runtime.artifacts_dir || "./artifacts";
|
|
1900
|
+
const store: ObjectStore = createProjectObjectStore(cfg, artifactsDir);
|
|
1901
|
+
const projectId = cfg.project?.id ?? "default";
|
|
1902
|
+
const record = {
|
|
1903
|
+
...caseSet,
|
|
1904
|
+
projectId,
|
|
1905
|
+
generatedAt: caseSet.generatedAt ?? new Date().toISOString(),
|
|
1906
|
+
};
|
|
1907
|
+
const objectKey = `${projectPrefix(projectId)}/test-cases/${caseSet.caseSetId}.json`;
|
|
1908
|
+
await store.put(objectKey, JSON.stringify(record, null, 2) + "\n", "application/json");
|
|
1909
|
+
return {
|
|
1910
|
+
status: "success",
|
|
1911
|
+
caseSetId: caseSet.caseSetId,
|
|
1912
|
+
journeyId: caseSet.journeyId,
|
|
1913
|
+
caseCount: caseSet.cases.length,
|
|
1914
|
+
objectKey,
|
|
1915
|
+
nextSteps: [
|
|
1916
|
+
`Plan the journey with bugmole_plan for "${caseSet.journeyId}" so these cases can be executed`,
|
|
1917
|
+
],
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
case "bugmole_write_test_case_results": {
|
|
1922
|
+
const resultSet = args.resultSet;
|
|
1923
|
+
if (!resultSet || typeof resultSet !== "object" || Array.isArray(resultSet)) {
|
|
1924
|
+
throw new Error("resultSet must be an object matching test-case-results.schema.json");
|
|
1925
|
+
}
|
|
1926
|
+
if (resultSet.schemaVersion !== 1 || typeof resultSet.caseSetId !== "string" || !resultSet.caseSetId) {
|
|
1927
|
+
throw new Error("resultSet.schemaVersion must be 1 and resultSet.caseSetId is required");
|
|
1928
|
+
}
|
|
1929
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(resultSet.caseSetId)) {
|
|
1930
|
+
throw new Error("resultSet.caseSetId may contain only letters, numbers, ., _, and -");
|
|
1931
|
+
}
|
|
1932
|
+
if (!Array.isArray(resultSet.results) || resultSet.results.length === 0) {
|
|
1933
|
+
throw new Error("resultSet.results must be a non-empty array");
|
|
1934
|
+
}
|
|
1935
|
+
const artifactsDir = cfg.runtime.artifacts_dir || "./artifacts";
|
|
1936
|
+
const store: ObjectStore = createProjectObjectStore(cfg, artifactsDir);
|
|
1937
|
+
const projectId = cfg.project?.id ?? "default";
|
|
1938
|
+
// Ground every verdict in the case set it claims to judge. Without this
|
|
1939
|
+
// a run could record a pass for a caseId nobody ever wrote, which is
|
|
1940
|
+
// the same failure mode as inventing routes — except it would show up
|
|
1941
|
+
// on the dashboard as proof.
|
|
1942
|
+
const caseSetKey = `${projectPrefix(projectId)}/test-cases/${resultSet.caseSetId}.json`;
|
|
1943
|
+
let storedCaseSet: any;
|
|
1944
|
+
try {
|
|
1945
|
+
storedCaseSet = JSON.parse(await store.getText(caseSetKey));
|
|
1946
|
+
} catch {
|
|
1947
|
+
throw new Error(
|
|
1948
|
+
`No test cases found for caseSetId "${resultSet.caseSetId}" — write them with bugmole_write_test_cases first`,
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
if (Number.isInteger(resultSet.journeyRevision)
|
|
1952
|
+
&& resultSet.journeyRevision !== storedCaseSet.journeyRevision) {
|
|
1953
|
+
throw new Error(
|
|
1954
|
+
`resultSet.journeyRevision (${resultSet.journeyRevision}) does not match the case set revision `
|
|
1955
|
+
+ `(${storedCaseSet.journeyRevision}) — re-run against the current assumptions`,
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
const knownCaseIds = new Set((storedCaseSet.cases ?? []).map((item: any) => item.caseId));
|
|
1959
|
+
const allowedStatuses = ["passed", "failed", "blocked", "needs_setup"];
|
|
1960
|
+
const seen = new Set<string>();
|
|
1961
|
+
for (const result of resultSet.results) {
|
|
1962
|
+
if (!result || typeof result !== "object") throw new Error("each result must be an object");
|
|
1963
|
+
if (typeof result.caseId !== "string" || !knownCaseIds.has(result.caseId)) {
|
|
1964
|
+
throw new Error(`result caseId "${result.caseId}" is not a case of ${resultSet.caseSetId}`);
|
|
1965
|
+
}
|
|
1966
|
+
if (seen.has(result.caseId)) throw new Error(`duplicate result for caseId: ${result.caseId}`);
|
|
1967
|
+
seen.add(result.caseId);
|
|
1968
|
+
if (!allowedStatuses.includes(result.status)) {
|
|
1969
|
+
throw new Error(`result ${result.caseId}: status must be one of ${allowedStatuses.join(", ")}`);
|
|
1970
|
+
}
|
|
1971
|
+
// A verdict with no detail is not reviewable, and "passed" with no
|
|
1972
|
+
// detail is exactly the shape of a fabricated result.
|
|
1973
|
+
if (typeof result.detail !== "string" || !result.detail.trim()) {
|
|
1974
|
+
throw new Error(`result ${result.caseId}: detail is required — say what was observed`);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
const record = {
|
|
1978
|
+
...resultSet,
|
|
1979
|
+
projectId,
|
|
1980
|
+
journeyId: storedCaseSet.journeyId,
|
|
1981
|
+
journeyRevision: storedCaseSet.journeyRevision,
|
|
1982
|
+
recordedAt: resultSet.recordedAt ?? new Date().toISOString(),
|
|
1983
|
+
};
|
|
1984
|
+
const objectKey = `${projectPrefix(projectId)}/test-case-results/${resultSet.caseSetId}.json`;
|
|
1985
|
+
await store.put(objectKey, JSON.stringify(record, null, 2) + "\n", "application/json");
|
|
1986
|
+
const counts = resultSet.results.reduce((acc: Record<string, number>, item: any) => {
|
|
1987
|
+
acc[item.status] = (acc[item.status] ?? 0) + 1;
|
|
1988
|
+
return acc;
|
|
1989
|
+
}, {});
|
|
1990
|
+
return {
|
|
1991
|
+
status: "success",
|
|
1992
|
+
caseSetId: resultSet.caseSetId,
|
|
1993
|
+
journeyId: storedCaseSet.journeyId,
|
|
1994
|
+
recorded: resultSet.results.length,
|
|
1995
|
+
of: knownCaseIds.size,
|
|
1996
|
+
counts,
|
|
1997
|
+
objectKey,
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
case "bugmole_write_ui_audit": {
|
|
2002
|
+
const audit = args.audit;
|
|
2003
|
+
if (!audit || typeof audit !== "object" || Array.isArray(audit)) {
|
|
2004
|
+
throw new Error("audit must be an object matching app-ui-audit.schema.json");
|
|
2005
|
+
}
|
|
2006
|
+
if (audit.schemaVersion !== 1 || typeof audit.auditId !== "string" || !audit.auditId) {
|
|
2007
|
+
throw new Error("audit.schemaVersion must be 1 and audit.auditId is required");
|
|
2008
|
+
}
|
|
2009
|
+
if (
|
|
2010
|
+
typeof audit.journeyId !== "string" ||
|
|
2011
|
+
!Number.isInteger(audit.journeyRevision) ||
|
|
2012
|
+
!["exhausted", "blocked"].includes(audit.flowStatus) ||
|
|
2013
|
+
!Array.isArray(audit.exploredNodeIds) ||
|
|
2014
|
+
!Array.isArray(audit.exploredEdgeIds)
|
|
2015
|
+
) {
|
|
2016
|
+
throw new Error(
|
|
2017
|
+
"audit requires journeyId, integer journeyRevision, flowStatus, exploredNodeIds, and exploredEdgeIds",
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(audit.auditId)) {
|
|
2021
|
+
throw new Error("audit.auditId may contain only letters, numbers, ., _, and -");
|
|
2022
|
+
}
|
|
2023
|
+
if (!Array.isArray(audit.screens) || !Array.isArray(audit.blockedVerification)) {
|
|
2024
|
+
throw new Error("audit.screens and audit.blockedVerification must be arrays");
|
|
2025
|
+
}
|
|
2026
|
+
const journeyGraph = readJourneyGraph(cfg);
|
|
2027
|
+
const journey = journeyGraph.journeys.find((item) => item.id === audit.journeyId);
|
|
2028
|
+
if (!journey) throw new Error(`Journey not found: ${audit.journeyId}`);
|
|
2029
|
+
if (journey.revision !== audit.journeyRevision) {
|
|
2030
|
+
throw new Error(
|
|
2031
|
+
`audit.journeyRevision must match the selected journey revision (${journey.revision})`,
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
const nextTask = await nextJourneyTaskWithClarification(cfg, audit.journeyId);
|
|
2035
|
+
if (nextTask.status === "actionable") {
|
|
2036
|
+
throw new Error("Cannot write an App Audit while the selected journey has an actionable next task");
|
|
2037
|
+
}
|
|
2038
|
+
if (audit.flowStatus !== nextTask.status) {
|
|
2039
|
+
throw new Error(`audit.flowStatus must match the selected journey state (${nextTask.status})`);
|
|
2040
|
+
}
|
|
2041
|
+
const allowedNodes = new Set(journey.nodeIds);
|
|
2042
|
+
const allowedEdges = new Set(journey.edgeIds);
|
|
2043
|
+
if (audit.exploredNodeIds.some((id: unknown) => typeof id !== "string" || !allowedNodes.has(id))) {
|
|
2044
|
+
throw new Error("audit.exploredNodeIds must reference nodes in the selected journey");
|
|
2045
|
+
}
|
|
2046
|
+
if (audit.exploredEdgeIds.some((id: unknown) => typeof id !== "string" || !allowedEdges.has(id))) {
|
|
2047
|
+
throw new Error("audit.exploredEdgeIds must reference edges in the selected journey");
|
|
2048
|
+
}
|
|
2049
|
+
const controls = audit.screens.flatMap((screen: any) => screen?.controls ?? []);
|
|
2050
|
+
const counts = {
|
|
2051
|
+
total: controls.length,
|
|
2052
|
+
implemented: controls.filter((control: any) => control.status === "Implemented").length,
|
|
2053
|
+
partiallyImplemented: controls.filter(
|
|
2054
|
+
(control: any) => control.status === "Partially implemented",
|
|
2055
|
+
).length,
|
|
2056
|
+
notImplemented: controls.filter((control: any) => control.status === "Not implemented")
|
|
2057
|
+
.length,
|
|
2058
|
+
};
|
|
2059
|
+
if (JSON.stringify(audit.summary) !== JSON.stringify(counts)) {
|
|
2060
|
+
throw new Error(`audit.summary must match control status totals: ${JSON.stringify(counts)}`);
|
|
2061
|
+
}
|
|
2062
|
+
const artifactsDir = cfg.runtime.artifacts_dir || "./artifacts";
|
|
2063
|
+
const store: ObjectStore = createProjectObjectStore(cfg, artifactsDir);
|
|
2064
|
+
for (const control of controls) {
|
|
2065
|
+
if (
|
|
2066
|
+
!control ||
|
|
2067
|
+
typeof control.controlId !== "string" ||
|
|
2068
|
+
typeof control.label !== "string" ||
|
|
2069
|
+
!["Implemented", "Partially implemented", "Not implemented"].includes(control.status) ||
|
|
2070
|
+
typeof control.destinationOrHandler !== "string" ||
|
|
2071
|
+
!Array.isArray(control.evidence) ||
|
|
2072
|
+
!Array.isArray(control.missingBehavior)
|
|
2073
|
+
) {
|
|
2074
|
+
throw new Error(
|
|
2075
|
+
"each control requires controlId, label, status, destinationOrHandler, evidence, and missingBehavior",
|
|
2076
|
+
);
|
|
2077
|
+
}
|
|
2078
|
+
// Evidence integrity gate: a control claiming Implemented/Partially
|
|
2079
|
+
// implemented status must back it with evidence that actually
|
|
2080
|
+
// exists, not a self-reported description with no artifact behind
|
|
2081
|
+
// it. Runtime evidence (as opposed to a source-code reference) is
|
|
2082
|
+
// only meaningful if the storage key it names is real.
|
|
2083
|
+
for (const evidence of control.evidence) {
|
|
2084
|
+
if (
|
|
2085
|
+
!evidence ||
|
|
2086
|
+
typeof evidence !== "object" ||
|
|
2087
|
+
!["runtime", "source"].includes(evidence.type) ||
|
|
2088
|
+
typeof evidence.description !== "string"
|
|
2089
|
+
) {
|
|
2090
|
+
throw new Error(
|
|
2091
|
+
`control ${control.controlId} has a malformed evidence entry; each entry requires type ("runtime" or "source") and description`,
|
|
2092
|
+
);
|
|
2093
|
+
}
|
|
2094
|
+
if (evidence.type === "runtime") {
|
|
2095
|
+
if (typeof evidence.location !== "string" || !evidence.location.trim()) {
|
|
2096
|
+
throw new Error(
|
|
2097
|
+
`control ${control.controlId} claims runtime evidence without a location; runtime evidence must reference a stored artifact key`,
|
|
2098
|
+
);
|
|
2099
|
+
}
|
|
2100
|
+
const exists = await store.has(evidence.location).catch(() => false);
|
|
2101
|
+
if (!exists) {
|
|
2102
|
+
throw new Error(
|
|
2103
|
+
`control ${control.controlId} references runtime evidence at "${evidence.location}", but no artifact exists at that key`,
|
|
2104
|
+
);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
const projectId = cfg.project?.id ?? audit.projectId ?? "default";
|
|
2110
|
+
const workspaceId =
|
|
2111
|
+
cfg.project?.workspace_id ??
|
|
2112
|
+
cfg.project?.workspaceId ??
|
|
2113
|
+
audit.workspaceId ??
|
|
2114
|
+
undefined;
|
|
2115
|
+
const auditKey = `${projectPrefix(projectId)}/ui-audits/${audit.auditId}.json`;
|
|
2116
|
+
const clarification = audit.clarification ?? nextTask.clarification;
|
|
2117
|
+
const blockedVerification = [...audit.blockedVerification];
|
|
2118
|
+
if (
|
|
2119
|
+
audit.flowStatus === "blocked" &&
|
|
2120
|
+
clarification &&
|
|
2121
|
+
!blockedVerification.some((item: any) => item?.fingerprint === clarification.fingerprint)
|
|
2122
|
+
) {
|
|
2123
|
+
blockedVerification.push({
|
|
2124
|
+
description: "Authenticated runtime verification requires user clarification.",
|
|
2125
|
+
reason: clarification.reason,
|
|
2126
|
+
fingerprint: clarification.fingerprint,
|
|
2127
|
+
question: clarification.question,
|
|
2128
|
+
status: clarification.status,
|
|
2129
|
+
...(clarification.resolutionId ? { resolutionId: clarification.resolutionId } : {}),
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
const payload = {
|
|
2133
|
+
...audit,
|
|
2134
|
+
projectId,
|
|
2135
|
+
blockedVerification,
|
|
2136
|
+
...(clarification ? { clarification } : {}),
|
|
2137
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
2138
|
+
};
|
|
2139
|
+
await store.put(auditKey, json(payload), "application/json");
|
|
2140
|
+
return {
|
|
2141
|
+
status: "success",
|
|
2142
|
+
auditId: audit.auditId,
|
|
2143
|
+
artifact: { key: auditKey, contentType: "application/json" },
|
|
2144
|
+
schema: "bugmole://spec/app-ui-audit.schema.json",
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
case "bugmole_write_ui_parity_audit": {
|
|
2149
|
+
const audit = args.audit;
|
|
2150
|
+
if (!audit || typeof audit !== "object" || Array.isArray(audit)) {
|
|
2151
|
+
throw new Error("audit must be an object matching ui-parity-audit.schema.json");
|
|
2152
|
+
}
|
|
2153
|
+
if (
|
|
2154
|
+
audit.schemaVersion !== 1 ||
|
|
2155
|
+
typeof audit.auditId !== "string" ||
|
|
2156
|
+
!audit.auditId ||
|
|
2157
|
+
!audit.reference ||
|
|
2158
|
+
!audit.current ||
|
|
2159
|
+
!audit.summary
|
|
2160
|
+
) {
|
|
2161
|
+
throw new Error(
|
|
2162
|
+
"audit requires schemaVersion 1, auditId, reference, current, and summary",
|
|
2163
|
+
);
|
|
2164
|
+
}
|
|
2165
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(audit.auditId)) {
|
|
2166
|
+
throw new Error("audit.auditId may contain only letters, numbers, ., _, and -");
|
|
2167
|
+
}
|
|
2168
|
+
if (
|
|
2169
|
+
!Array.isArray(audit.flowMatrix) ||
|
|
2170
|
+
!Array.isArray(audit.comparisons) ||
|
|
2171
|
+
!Array.isArray(audit.issues) ||
|
|
2172
|
+
typeof audit.fixPrompt !== "string" ||
|
|
2173
|
+
!Array.isArray(audit.blockedVerification)
|
|
2174
|
+
) {
|
|
2175
|
+
throw new Error(
|
|
2176
|
+
"audit.flowMatrix, comparisons, issues, blockedVerification, and fixPrompt are required",
|
|
2177
|
+
);
|
|
2178
|
+
}
|
|
2179
|
+
const scoreFields = [
|
|
2180
|
+
"visualDifferencePercent",
|
|
2181
|
+
"interactionParityScore",
|
|
2182
|
+
"apiParityScore",
|
|
2183
|
+
"authenticationParityScore",
|
|
2184
|
+
];
|
|
2185
|
+
for (const field of scoreFields) {
|
|
2186
|
+
if (
|
|
2187
|
+
typeof audit.summary[field] !== "number" ||
|
|
2188
|
+
audit.summary[field] < 0 ||
|
|
2189
|
+
audit.summary[field] > 100
|
|
2190
|
+
) {
|
|
2191
|
+
throw new Error(`audit.summary.${field} must be a number from 0 to 100`);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
const artifactsDir = cfg.runtime.artifacts_dir || "./artifacts";
|
|
2195
|
+
const store: ObjectStore = createProjectObjectStore(cfg, artifactsDir);
|
|
2196
|
+
const projectId = cfg.project?.id ?? audit.projectId ?? "default";
|
|
2197
|
+
const workspaceId =
|
|
2198
|
+
cfg.project?.workspace_id ??
|
|
2199
|
+
cfg.project?.workspaceId ??
|
|
2200
|
+
audit.workspaceId ??
|
|
2201
|
+
undefined;
|
|
2202
|
+
const appId = cfg.project?.app_id ?? cfg.project?.appId ?? audit.appId ?? undefined;
|
|
2203
|
+
const parityKey = `${projectPrefix(projectId)}/ui-parity-audits/${audit.auditId}.json`;
|
|
2204
|
+
const payload = {
|
|
2205
|
+
...audit,
|
|
2206
|
+
projectId,
|
|
2207
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
2208
|
+
...(appId ? { appId } : {}),
|
|
2209
|
+
};
|
|
2210
|
+
await store.put(parityKey, json(payload), "application/json");
|
|
2211
|
+
const taskApiKey = process.env.BUGMOLE_API_KEY ?? cfg.registry?.api_key;
|
|
2212
|
+
if (taskApiKey) {
|
|
2213
|
+
const client = taskClient(cfg);
|
|
2214
|
+
const issues = [
|
|
2215
|
+
...((audit.issues ?? []).filter((issue: any) =>
|
|
2216
|
+
["unresolved", "blocked", "missing_backend", "platform_limitation"].includes(issue.status))),
|
|
2217
|
+
...((audit.blockedVerification ?? []).map((blocked: any) => ({
|
|
2218
|
+
issueId: `blocked:${blocked.description}`,
|
|
2219
|
+
category: "platform",
|
|
2220
|
+
status: "blocked",
|
|
2221
|
+
severity: "high",
|
|
2222
|
+
description: `${blocked.description}: ${blocked.reason}`,
|
|
2223
|
+
}))),
|
|
2224
|
+
];
|
|
2225
|
+
await Promise.all(issues.map((issue: any) => client.createTask({
|
|
2226
|
+
title: `Resolve parity: ${String(issue.description).slice(0, 100)}`,
|
|
2227
|
+
description: String(issue.description),
|
|
2228
|
+
taskType: "ui",
|
|
2229
|
+
priority: issue.severity === "high" ? 10 : issue.severity === "medium" ? 5 : 1,
|
|
2230
|
+
idempotencyKey: `${audit.auditId}:${issue.issueId}`,
|
|
2231
|
+
})));
|
|
2232
|
+
}
|
|
2233
|
+
return {
|
|
2234
|
+
status: "success",
|
|
2235
|
+
auditId: audit.auditId,
|
|
2236
|
+
artifact: { key: parityKey, contentType: "application/json" },
|
|
2237
|
+
schema: "bugmole://spec/ui-parity-audit.schema.json",
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
case "bugmole_read_spec":
|
|
2242
|
+
return readSpecFile(args.filePath, cfg);
|
|
2243
|
+
|
|
2244
|
+
case "bugmole_write_spec":
|
|
2245
|
+
return writeSpecFile(args.filePath, args.content, cfg);
|
|
2246
|
+
|
|
2247
|
+
case "bugmole_report_progress": {
|
|
2248
|
+
// Store progress report for monitoring
|
|
2249
|
+
const artifactsDir = cfg.runtime.artifacts_dir || "./artifacts";
|
|
2250
|
+
const store: ObjectStore = createProjectObjectStore(cfg, artifactsDir);
|
|
2251
|
+
|
|
2252
|
+
const progressId = `progress_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
2253
|
+
const progressKey = `${projectPrefix(cfg.project?.id ?? "default")}/progress/${progressId}.json`;
|
|
2254
|
+
|
|
2255
|
+
const progressData = {
|
|
2256
|
+
id: progressId,
|
|
2257
|
+
timestamp: new Date().toISOString(),
|
|
2258
|
+
status: args.status,
|
|
2259
|
+
message: args.message,
|
|
2260
|
+
details: args.details || "",
|
|
2261
|
+
estimated_time_remaining: args.estimated_time_remaining || "",
|
|
2262
|
+
};
|
|
2263
|
+
|
|
2264
|
+
await store.put(progressKey, json(progressData), "application/json");
|
|
2265
|
+
|
|
2266
|
+
return {
|
|
2267
|
+
success: true,
|
|
2268
|
+
progressId,
|
|
2269
|
+
message: "Progress reported",
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
case "bugmole_task_next": {
|
|
2274
|
+
const client = taskClient(cfg);
|
|
2275
|
+
const listed = await client.listTasks();
|
|
2276
|
+
const next = listed.tasks.find((task) => isClaimableTask(task));
|
|
2277
|
+
if (!next) return { task: null, message: "No queued agent tasks are available." };
|
|
2278
|
+
return client.claimTask(next.id, args.workerId);
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
case "bugmole_task_create": {
|
|
2282
|
+
if (typeof args.idempotencyKey !== "string" || !args.idempotencyKey.trim()) {
|
|
2283
|
+
throw new Error("idempotencyKey is required so a re-proposed follow-up cannot duplicate work");
|
|
2284
|
+
}
|
|
2285
|
+
const client = taskClient(cfg);
|
|
2286
|
+
return client.createTask({
|
|
2287
|
+
title: args.title,
|
|
2288
|
+
description: args.description,
|
|
2289
|
+
taskType: args.taskType,
|
|
2290
|
+
priority: args.priority,
|
|
2291
|
+
idempotencyKey: args.idempotencyKey.trim(),
|
|
2292
|
+
journeyId: args.journeyId,
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
case "bugmole_task_update": {
|
|
2297
|
+
const client = taskClient(cfg);
|
|
2298
|
+
const update = { ...args };
|
|
2299
|
+
delete update.taskId;
|
|
2300
|
+
return client.updateTask(args.taskId, update);
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
case "bugmole_commit_response": {
|
|
2304
|
+
// Store the committed response for external agent consumption
|
|
2305
|
+
// This is the completion signal for CURSOR_DRIVER tasks
|
|
2306
|
+
const artifactsDir = cfg.runtime.artifacts_dir || "./artifacts";
|
|
2307
|
+
const store: ObjectStore = createProjectObjectStore(cfg, artifactsDir);
|
|
2308
|
+
|
|
2309
|
+
const commitId = `commit_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
2310
|
+
const commitKey = `${projectPrefix(cfg.project?.id ?? "default")}/cursor-commits/${commitId}.json`;
|
|
2311
|
+
|
|
2312
|
+
// Validate and limit payload size to prevent tool call interruptions
|
|
2313
|
+
// Large payloads can cause stdio buffer issues
|
|
2314
|
+
const MAX_PAYLOAD_SIZE = 500000; // ~500KB limit for safety
|
|
2315
|
+
const MAX_ANSWER_SIZE = 200000; // ~200KB for answer_markdown
|
|
2316
|
+
const MAX_ARTIFACT_CONTENT = 100000; // ~100KB per artifact content
|
|
2317
|
+
|
|
2318
|
+
let answer_markdown = args.answer_markdown || "";
|
|
2319
|
+
if (answer_markdown.length > MAX_ANSWER_SIZE) {
|
|
2320
|
+
console.warn(
|
|
2321
|
+
`[Bugmole MCP] answer_markdown is too large (${answer_markdown.length} chars), truncating to ${MAX_ANSWER_SIZE}`,
|
|
2322
|
+
);
|
|
2323
|
+
answer_markdown =
|
|
2324
|
+
answer_markdown.substring(0, MAX_ANSWER_SIZE) + "\n\n... (truncated due to size limit)";
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
// Truncate artifact content if too large
|
|
2328
|
+
const artifacts = (args.artifacts || []).map((artifact: any) => {
|
|
2329
|
+
if (artifact.content && artifact.content.length > MAX_ARTIFACT_CONTENT) {
|
|
2330
|
+
console.warn(
|
|
2331
|
+
`[Bugmole MCP] Artifact content is too large (${artifact.content.length} chars), truncating`,
|
|
2332
|
+
);
|
|
2333
|
+
return {
|
|
2334
|
+
...artifact,
|
|
2335
|
+
content:
|
|
2336
|
+
artifact.content.substring(0, MAX_ARTIFACT_CONTENT) +
|
|
2337
|
+
"\n\n... (truncated due to size limit)",
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
return artifact;
|
|
2341
|
+
});
|
|
2342
|
+
|
|
2343
|
+
// Handle optional fields gracefully - provide defaults for arrays
|
|
2344
|
+
// Normalize next_steps: convert string to array, null/undefined to empty array
|
|
2345
|
+
// Check both next_steps and nextSteps (in case of camelCase)
|
|
2346
|
+
const nextStepsArg = args.next_steps !== undefined ? args.next_steps : args.nextSteps;
|
|
2347
|
+
|
|
2348
|
+
// Log to file for debugging (since stdio mode redirects console.log to stderr)
|
|
2349
|
+
const logFile = path.join(artifactsDir, "mcp_debug.log");
|
|
2350
|
+
const logEntry = {
|
|
2351
|
+
timestamp: new Date().toISOString(),
|
|
2352
|
+
tool: "bugmole_commit_response",
|
|
2353
|
+
allArgsKeys: Object.keys(args),
|
|
2354
|
+
next_steps_received: args.next_steps,
|
|
2355
|
+
nextSteps_received: args.nextSteps,
|
|
2356
|
+
nextStepsArg: nextStepsArg,
|
|
2357
|
+
nextStepsArgType: typeof nextStepsArg,
|
|
2358
|
+
nextStepsArgIsArray: Array.isArray(nextStepsArg),
|
|
2359
|
+
fullArgs: args,
|
|
2360
|
+
};
|
|
2361
|
+
try {
|
|
2362
|
+
// Ensure directory exists
|
|
2363
|
+
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
2364
|
+
fs.appendFileSync(logFile, JSON.stringify(logEntry, null, 2) + "\n---\n", "utf-8");
|
|
2365
|
+
// Also log to console that we wrote the file
|
|
2366
|
+
console.log("[Bugmole MCP] Debug log written to:", logFile);
|
|
2367
|
+
} catch (e: any) {
|
|
2368
|
+
// Log the error so we can see what went wrong
|
|
2369
|
+
console.error("[Bugmole MCP] Failed to write debug log:", e.message, "path:", logFile);
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
console.log("[Bugmole MCP] All args keys:", Object.keys(args));
|
|
2373
|
+
console.log(
|
|
2374
|
+
"[Bugmole MCP] Received next_steps:",
|
|
2375
|
+
JSON.stringify(nextStepsArg),
|
|
2376
|
+
"type:",
|
|
2377
|
+
typeof nextStepsArg,
|
|
2378
|
+
"isArray:",
|
|
2379
|
+
Array.isArray(nextStepsArg),
|
|
2380
|
+
"args.next_steps:",
|
|
2381
|
+
JSON.stringify(args.next_steps),
|
|
2382
|
+
"args.nextSteps:",
|
|
2383
|
+
JSON.stringify(args.nextSteps),
|
|
2384
|
+
);
|
|
2385
|
+
|
|
2386
|
+
let next_steps: string[] = [];
|
|
2387
|
+
if (nextStepsArg !== undefined && nextStepsArg !== null) {
|
|
2388
|
+
if (Array.isArray(nextStepsArg)) {
|
|
2389
|
+
next_steps = nextStepsArg.filter((s: any) => s && typeof s === "string");
|
|
2390
|
+
console.log("[Bugmole MCP] Normalized next_steps from array:", next_steps);
|
|
2391
|
+
} else if (typeof nextStepsArg === "string") {
|
|
2392
|
+
// If agent passes a single string, convert to array
|
|
2393
|
+
next_steps = [nextStepsArg];
|
|
2394
|
+
console.log("[Bugmole MCP] Normalized next_steps from string:", next_steps);
|
|
2395
|
+
} else {
|
|
2396
|
+
console.warn(
|
|
2397
|
+
"[Bugmole MCP] next_steps has unexpected type:",
|
|
2398
|
+
typeof nextStepsArg,
|
|
2399
|
+
"value:",
|
|
2400
|
+
nextStepsArg,
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
} else if (nextStepsArg === undefined) {
|
|
2404
|
+
console.log("[Bugmole MCP] next_steps is undefined, using empty array");
|
|
2405
|
+
|
|
2406
|
+
// Fallback: Try to extract next_steps from answer_markdown if it mentions "Next Steps"
|
|
2407
|
+
if (
|
|
2408
|
+
answer_markdown &&
|
|
2409
|
+
(answer_markdown.includes("Next Steps") || answer_markdown.includes("next steps"))
|
|
2410
|
+
) {
|
|
2411
|
+
console.log("[Bugmole MCP] Attempting to extract next_steps from answer_markdown");
|
|
2412
|
+
// Try multiple patterns: ## Next Steps, ### Next Steps, **Next Steps**, etc.
|
|
2413
|
+
// Handle emoji and various markdown formats
|
|
2414
|
+
const nextStepsMatch =
|
|
2415
|
+
answer_markdown.match(
|
|
2416
|
+
/(?:##|###|\*\*)\s*[📋\s]*Next Steps\s*(?:\*\*)?\s*\n([\s\S]*?)(?=\n(?:##|###|---|$))/i,
|
|
2417
|
+
) || answer_markdown.match(/Next Steps\s*\n([\s\S]*?)(?=\n(?:##|###|---|$))/i);
|
|
2418
|
+
if (nextStepsMatch) {
|
|
2419
|
+
const nextStepsText = nextStepsMatch[1];
|
|
2420
|
+
console.log(
|
|
2421
|
+
"[Bugmole MCP] Extracted next steps text:",
|
|
2422
|
+
nextStepsText.substring(0, 200),
|
|
2423
|
+
);
|
|
2424
|
+
// Extract numbered or bulleted items
|
|
2425
|
+
const steps = nextStepsText
|
|
2426
|
+
.split(/\n/)
|
|
2427
|
+
.map((line: string) => line.trim())
|
|
2428
|
+
.filter((line: string) => {
|
|
2429
|
+
// Match numbered lists (1., 2., etc.) or bullet points (-, *, •)
|
|
2430
|
+
// Skip sub-items (lines starting with - or spaces)
|
|
2431
|
+
if (/^\s+[-*•]/.test(line)) return false;
|
|
2432
|
+
return /^\d+\.\s/.test(line) || /^[-*•]\s/.test(line);
|
|
2433
|
+
})
|
|
2434
|
+
.map((line: string) => {
|
|
2435
|
+
// Extract the step text (remove numbering/bullet and markdown)
|
|
2436
|
+
// Handle "1. **Bold text**" format
|
|
2437
|
+
const boldMatch = line.match(/^\d+\.\s*\*\*(.+?)\*\*/);
|
|
2438
|
+
if (boldMatch) {
|
|
2439
|
+
return boldMatch[1].trim();
|
|
2440
|
+
}
|
|
2441
|
+
// Handle "1. Regular text" format
|
|
2442
|
+
const numMatch = line.match(/^\d+\.\s+(.+)$/);
|
|
2443
|
+
if (numMatch) {
|
|
2444
|
+
return numMatch[1].trim();
|
|
2445
|
+
}
|
|
2446
|
+
// Handle bullet points
|
|
2447
|
+
const bulletMatch = line.match(/^[-*•]\s+(.+)$/);
|
|
2448
|
+
if (bulletMatch) {
|
|
2449
|
+
return bulletMatch[1].trim();
|
|
2450
|
+
}
|
|
2451
|
+
// Fallback: remove numbering/bullet
|
|
2452
|
+
return line
|
|
2453
|
+
.replace(/^\d+\.\s*/, "")
|
|
2454
|
+
.replace(/^[-*•]\s*/, "")
|
|
2455
|
+
.trim();
|
|
2456
|
+
})
|
|
2457
|
+
.filter((step: string) => step.length > 0 && !step.match(/^[-*•\s]*$/));
|
|
2458
|
+
|
|
2459
|
+
if (steps.length > 0) {
|
|
2460
|
+
console.log("[Bugmole MCP] Extracted next_steps from answer_markdown:", steps);
|
|
2461
|
+
next_steps = steps;
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
} else {
|
|
2466
|
+
console.log("[Bugmole MCP] next_steps is null, using empty array");
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
console.log("[Bugmole MCP] Final next_steps to save:", next_steps);
|
|
2470
|
+
|
|
2471
|
+
// Ensure next_steps is always an array (never undefined)
|
|
2472
|
+
const final_next_steps = Array.isArray(next_steps) ? next_steps : [];
|
|
2473
|
+
|
|
2474
|
+
const commitData = {
|
|
2475
|
+
id: commitId,
|
|
2476
|
+
timestamp: new Date().toISOString(),
|
|
2477
|
+
payload: {
|
|
2478
|
+
status: args.status,
|
|
2479
|
+
summary: args.summary,
|
|
2480
|
+
...(answer_markdown ? { answer_markdown } : {}),
|
|
2481
|
+
actions: Array.isArray(args.actions) ? args.actions : [],
|
|
2482
|
+
artifacts: Array.isArray(artifacts) ? artifacts : [],
|
|
2483
|
+
next_steps: final_next_steps, // Always include as array, never undefined
|
|
2484
|
+
},
|
|
2485
|
+
};
|
|
2486
|
+
|
|
2487
|
+
// Debug: Verify next_steps is in the payload
|
|
2488
|
+
console.log("[Bugmole MCP] Commit payload keys:", Object.keys(commitData.payload));
|
|
2489
|
+
console.log("[Bugmole MCP] Commit payload next_steps:", commitData.payload.next_steps);
|
|
2490
|
+
|
|
2491
|
+
// Debug: Log what we're about to save
|
|
2492
|
+
console.log("[Bugmole MCP] Commit data payload keys:", Object.keys(commitData.payload));
|
|
2493
|
+
console.log(
|
|
2494
|
+
"[Bugmole MCP] Commit data next_steps value:",
|
|
2495
|
+
JSON.stringify(commitData.payload.next_steps),
|
|
2496
|
+
);
|
|
2497
|
+
console.log(
|
|
2498
|
+
"[Bugmole MCP] Commit data has next_steps:",
|
|
2499
|
+
"next_steps" in commitData.payload,
|
|
2500
|
+
);
|
|
2501
|
+
|
|
2502
|
+
// Check total payload size
|
|
2503
|
+
const payloadString = JSON.stringify(commitData);
|
|
2504
|
+
if (payloadString.length > MAX_PAYLOAD_SIZE) {
|
|
2505
|
+
console.warn(
|
|
2506
|
+
`[Bugmole MCP] Total payload size (${payloadString.length} chars) exceeds limit (${MAX_PAYLOAD_SIZE}), truncating answer_markdown further`,
|
|
2507
|
+
);
|
|
2508
|
+
// Further truncate answer_markdown to fit
|
|
2509
|
+
const targetSize = MAX_PAYLOAD_SIZE - (payloadString.length - answer_markdown.length);
|
|
2510
|
+
commitData.payload.answer_markdown = answer_markdown.substring(
|
|
2511
|
+
0,
|
|
2512
|
+
Math.max(0, targetSize - 1000),
|
|
2513
|
+
);
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
await store.put(commitKey, json(commitData), "application/json");
|
|
2517
|
+
|
|
2518
|
+
// Return minimal response to avoid stdio buffer issues
|
|
2519
|
+
// The full payload is stored in the file and will be read by the monitor
|
|
2520
|
+
// Keep response under 1KB to ensure it doesn't get truncated
|
|
2521
|
+
return {
|
|
2522
|
+
success: true,
|
|
2523
|
+
commitId,
|
|
2524
|
+
message: "Committed",
|
|
2525
|
+
};
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
default:
|
|
2529
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
export async function readResource(params: any, cfg: any): Promise<any> {
|
|
2534
|
+
const { uri } = params;
|
|
2535
|
+
const specDir = cfg.runtime.spec_dir || "./spec";
|
|
2536
|
+
|
|
2537
|
+
// Parse bugmole:// URIs
|
|
2538
|
+
if (uri === "bugmole://guidance/agent-testing") {
|
|
2539
|
+
return {
|
|
2540
|
+
contents: [
|
|
2541
|
+
{
|
|
2542
|
+
uri,
|
|
2543
|
+
mimeType: "text/markdown",
|
|
2544
|
+
text: [
|
|
2545
|
+
"# Bugmole guided agent testing",
|
|
2546
|
+
"",
|
|
2547
|
+
"Subscribe to this MCP server for continuous guidance. Keep the MCP session open and use `bugmole_report_progress` every 2–3 minutes or after each exploration phase.",
|
|
2548
|
+
"",
|
|
2549
|
+
"Required sequence:",
|
|
2550
|
+
"1. Read this resource and the roles, domain rules, blockers, and journey graph resources.",
|
|
2551
|
+
"2. Inspect the repository structure, package/config files, application entrypoints, routes, and relevant source/spec files.",
|
|
2552
|
+
"3. Call `bugmole_environment_get` for the selected environment. If it is missing or incomplete, call `bugmole_environment_set` with the safe base URL, mock/reference URL, device transport, device label, and reproducible start command before exploring.",
|
|
2553
|
+
"4. For an ADB environment, verify the named device is reachable and follow the recorded start command; do not guess a generic browser URL. For Lumis android-local, the expected transport is Nokia via ADB and the mock/reference URL is `https://gcore.otlrs.com/lumis-consumer/`.",
|
|
2554
|
+
"5. Call `bugmole_explore` with the environment ID; omit baseUrl when the environment contract provides it.",
|
|
2555
|
+
"6. If an authenticated browser or device session is available, explore one concrete task and call `bugmole_record_discovery` with screens, transitions, proof tokens, `verifiedControlIds` for every button/link/action checked, and `completedNodeIds` after the terminal action actually succeeds.",
|
|
2556
|
+
"7. After every exploration or discovery result, call `bugmole_journey_get` and `bugmole_journey_next` for the selected flow.",
|
|
2557
|
+
"8. Perform the returned nextTask, add/edit the flow with `bugmole_journey_add_step` or `bugmole_journey_add_transition`, then call `bugmole_journey_next` again.",
|
|
2558
|
+
"9. If the chart returns blocked, inspect its clarification question and check the remembered resolution. Ask the user when it is missing or stale; record their answer with `bugmole_resolve_blocker`, then retry the chart.",
|
|
2559
|
+
"10. Continue this chart-check loop until every transition is runtime-verified, the terminal goal is completed, and `bugmole_journey_next` returns exhausted or an explicit blocked reason that cannot be resolved.",
|
|
2560
|
+
"11. Once the flow is exhausted, write its test case assumptions: read `bugmole://spec/test-cases.schema.json` and call `bugmole_write_test_cases` with one case per step worth proving. Each case must name a real `nodeId` from that journey, state what it is trying to prove, and list observable expectations. Cite what each assumption is based on in `basis`; a case with nothing behind it must be `confidence: \"low\"` rather than a confident guess.",
|
|
2561
|
+
"12. Confirm the selected actor exists in `roles.yaml`, then call `bugmole_plan` only after the flow is exhausted or documented as blocked.",
|
|
2562
|
+
"13. After a run actually exercises those cases, record what it observed: read `bugmole://spec/test-case-results.schema.json` and call `bugmole_write_test_case_results` with one result per case you checked. This is the only thing that moves a case off \"planned\" — writing assumptions never counts as proof, so a flow whose cases were verified but never recorded reads as 0% covered. Say what you saw in `detail` (status code, element present or missing, assertion that failed), use \"failed\" when a check ran and the expectation did not hold, and \"blocked\" only when it could not be run at all. Do not record a result for a case you did not actually exercise.",
|
|
2563
|
+
"14. Commit progress/results with `bugmole_commit_response`, always including the returned `nextSteps` until the workflow is complete.",
|
|
2564
|
+
"",
|
|
2565
|
+
"A plan with no discovered journey evidence, a generic fallback action, or an unchecked actionable flow task is incomplete and must not be published as ready to run.",
|
|
2566
|
+
].join("\n"),
|
|
2567
|
+
},
|
|
2568
|
+
],
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
if (uri === "bugmole://guidance/app-ui-audit") {
|
|
2572
|
+
return {
|
|
2573
|
+
contents: [
|
|
2574
|
+
{
|
|
2575
|
+
uri,
|
|
2576
|
+
mimeType: "text/markdown",
|
|
2577
|
+
text: [
|
|
2578
|
+
"# Bugmole application UI audit",
|
|
2579
|
+
"",
|
|
2580
|
+
"Start by populating the component library, because this task is the only thing that does. Call `bugmole_ui_re_start` with kind `existing_app` and the application's base URL, then `bugmole_ui_re_stage` with stage `atoms`. That runs the Storybook/CSF source scan and records the catalogue the dashboard's component library and the step sheet's component chips read. If the project has no Storybook, the scan returns a blocked reason — report that reason as-is; do not invent components to fill the library.",
|
|
2581
|
+
"",
|
|
2582
|
+
"Then reproduce the requested user flow in the running product and audit every user-facing button, link, row, card, and other interactive control.",
|
|
2583
|
+
"",
|
|
2584
|
+
"For each screen, group and report every control with:",
|
|
2585
|
+
"1. Visible label or action",
|
|
2586
|
+
"2. Status: Implemented, Partially implemented, or Not implemented",
|
|
2587
|
+
"3. Destination, callback, or handler",
|
|
2588
|
+
"4. Runtime or source-code evidence",
|
|
2589
|
+
"5. Missing backend, persistence, validation, or integration behavior",
|
|
2590
|
+
"",
|
|
2591
|
+
"Use these definitions:",
|
|
2592
|
+
"- Implemented: the intended action works end-to-end.",
|
|
2593
|
+
"- Partially implemented: the UI responds, but some persistence, backend, validation, or integration is incomplete.",
|
|
2594
|
+
"- Not implemented: the control is dead, has a no-op handler, only shows a placeholder/mock message, or has no working destination.",
|
|
2595
|
+
"",
|
|
2596
|
+
"Include navigation controls, tabs, forms, selectable cards, external links, CTAs, menu rows, and controls that visually appear interactive. Do not count static text as a link unless it has a real handler.",
|
|
2597
|
+
"",
|
|
2598
|
+
"Verify findings at runtime where possible, then inspect the source code to catch hidden or unreachable controls. Ignore unreachable legacy branches unless they affect the active user flow.",
|
|
2599
|
+
"",
|
|
2600
|
+
"Report:",
|
|
2601
|
+
"- A brief summary of totals by status",
|
|
2602
|
+
"- A complete screen-by-screen grouping",
|
|
2603
|
+
"- Evidence for each finding",
|
|
2604
|
+
"- The highest-priority missing implementations",
|
|
2605
|
+
"- Any blocked verification steps and why",
|
|
2606
|
+
"",
|
|
2607
|
+
"The canonical deliverable is JSON, not markdown. Read bugmole://spec/app-ui-audit.schema.json, build one object matching that schema, and call bugmole_write_ui_audit with the completed object. Do not omit screens with zero findings or blocked verification entries.",
|
|
2608
|
+
"The tool writes the dashboard-consumable file under the project-scoped ui-audits artifact prefix and returns its artifact key. You may additionally create a visual artifact or canvas for a large human-readable presentation, but it does not replace the JSON file.",
|
|
2609
|
+
].join("\n"),
|
|
2610
|
+
},
|
|
2611
|
+
],
|
|
2612
|
+
};
|
|
2613
|
+
}
|
|
2614
|
+
if (uri === "bugmole://guidance/ui-parity-audit") {
|
|
2615
|
+
return {
|
|
2616
|
+
contents: [
|
|
2617
|
+
{
|
|
2618
|
+
uri,
|
|
2619
|
+
mimeType: "text/markdown",
|
|
2620
|
+
text: [
|
|
2621
|
+
"# Bugmole UI parity audit",
|
|
2622
|
+
"",
|
|
2623
|
+
"Use this task only after the user supplies a reference experience (URL, HTML, old app link, screenshots, or Figma) and the current application has already been implemented.",
|
|
2624
|
+
"This task compares the reference and current implementation. It does not replace the application UI audit: do not produce a control-by-control implementation status report here.",
|
|
2625
|
+
"",
|
|
2626
|
+
"First create a flow matrix for every reachable reference screen, navigation path, interaction, state transition, authentication state, loading state, error state, and backend-dependent action.",
|
|
2627
|
+
"",
|
|
2628
|
+
"Explore the reference systematically, recording screens, actions, resulting behavior, navigation, unavailable or ambiguous behavior, and before/after screenshots for meaningful actions.",
|
|
2629
|
+
"Audit the current implementation by mapping reference screens/actions to routes, components, and service/API callers. Verify guest, authenticated, loading, empty, error, logout, deep-link, callback, persistence, and API-error states. Confirm displayed data is live rather than fixture data.",
|
|
2630
|
+
"Use Python with Pillow for the visual comparison. Capture the reference with a Playwright screenshot and the app with `adb exec-out screencap -p`. Normalize both to the same viewport, such as 390x844. Run `python scripts/ui-parity-diff.py --reference reference.png --actual actual.png --output-dir parity-output --width 390 --height 844 --threshold 16` to calculate `ImageChops.difference()`, count pixels whose channel difference exceeds the threshold, and generate a heatmap.",
|
|
2631
|
+
"Save the resulting `diff.json` and `heatmap.png` paths in the parity artifact. The difference score is a percentage: 0% means identical pixels and higher percentages mean more visual difference. Treat status bars, navigation bars, viewport scaling, fonts, and image loading as diagnostic factors rather than automatically as product defects.",
|
|
2632
|
+
"Separate visual, behavioral, backend, and authentication mismatches.",
|
|
2633
|
+
"Fix confirmed issues in place when authorized, preserve live integrations and design tokens, rebuild, replay changed flows, recapture screenshots, rerun comparisons, and inspect runtime/auth/network/API logs.",
|
|
2634
|
+
"",
|
|
2635
|
+
"The canonical deliverable is JSON. Read bugmole://spec/ui-parity-audit.schema.json, create a complete object including the flow matrix, reference/current captures, difference percentages, changed/total pixel counts, heatmaps, issues, artifact paths, blocked verification, and a concise fixPrompt for any remaining work, then call bugmole_write_ui_parity_audit.",
|
|
2636
|
+
"Tag each issue with the screenId of the comparison it was found on. The dashboard groups the bug list by step using this field; an issue left untagged is shown as ungrouped rather than guessed at.",
|
|
2637
|
+
"The tool writes the dashboard-consumable file under the project-scoped ui-parity-audits artifact prefix. The dashboard displays only this JSON artifact.",
|
|
2638
|
+
].join("\n"),
|
|
2639
|
+
},
|
|
2640
|
+
],
|
|
2641
|
+
};
|
|
2642
|
+
}
|
|
2643
|
+
if (uri === "bugmole://guidance/ui-reverse-engineering") {
|
|
2644
|
+
return {
|
|
2645
|
+
contents: [{
|
|
2646
|
+
uri,
|
|
2647
|
+
mimeType: "text/markdown",
|
|
2648
|
+
text: [
|
|
2649
|
+
"# Bugmole UI reverse-engineering coordinator",
|
|
2650
|
+
"",
|
|
2651
|
+
"Run the coordinator inside-out: normalize the reference, measure regions, infer or reuse the project Design DNA, extract atoms, compose molecules/organisms/templates/pages, then validate parity.",
|
|
2652
|
+
"Design DNA is the project genome. Match its spacing, radius, elevation, color, motion, icon, and typography tokens before creating a new primitive. Any extension must include provenance, confidence, and a reason existing tokens cannot satisfy the screen.",
|
|
2653
|
+
"Use bugmole_ui_re_start with an image, Figma export, PDF, URL, HTML, or existing-app input. Advance the resumable job with bugmole_ui_re_stage in this order: measure, dna, atoms, reconcile, complete.",
|
|
2654
|
+
"When a reference is a URL, open it in the browser and capture the rendered reference before analyzing it. Associate the capture with the relevant journey steps; do not treat the URL string itself as screenshot evidence.",
|
|
2655
|
+
"For each reachable journey node, try a reference capture first when the mockup is active and related. If it is archived or unrelated, capture the current app instead. If neither capture is possible, preserve the node and record an explicit blocked reason.",
|
|
2656
|
+
"Use bugmole_ui_re_status after every stage. Treat low-confidence measurements and unavailable adapters as blocked evidence, not as invented facts.",
|
|
2657
|
+
"Reuse canonical components and stable BEM names from the project playbook. Reconcile only the affected dependency branch after a mismatch.",
|
|
2658
|
+
"For full reference-versus-current comparison, continue with bugmole_write_ui_parity_audit and include Design DNA token references and token-drift mismatches.",
|
|
2659
|
+
].join("\n"),
|
|
2660
|
+
}],
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
2663
|
+
if (uri === "bugmole://guidance/agent-task-queue") {
|
|
2664
|
+
return {
|
|
2665
|
+
contents: [{
|
|
2666
|
+
uri,
|
|
2667
|
+
mimeType: "text/markdown",
|
|
2668
|
+
text: [
|
|
2669
|
+
"# Bugmole agent task queue",
|
|
2670
|
+
"",
|
|
2671
|
+
"Call bugmole_task_next with a stable workerId before starting queued work.",
|
|
2672
|
+
"Inspect the repository, implement the task, and report progress with bugmole_report_progress.",
|
|
2673
|
+
"If the task includes journeyId and journeyRevision, load that exact project-scoped journeys/graph.json revision and follow its branches in order.",
|
|
2674
|
+
"For taskType ui, read the parity guidance and schema, rerun affected flows, generate a fresh parity artifact, and include parityAuditId and parityMatchScore.",
|
|
2675
|
+
"A UI task cannot be completed without fresh parity evidence. If parity is blocked, leave the task blocked with concrete next steps.",
|
|
2676
|
+
"When finished, call bugmole_task_update with status completed, completionSummary, changedFiles, and required parity evidence.",
|
|
2677
|
+
"",
|
|
2678
|
+
"Finishing a task is not the end of the work. This queue drives an autonomous pipeline: explore, then define any missing actors in roles.yaml, then write test case assumptions with bugmole_write_test_cases, then bugmole_plan. If your task leaves an obvious next stage undone, queue it with bugmole_task_create before you complete, using a stable idempotencyKey of the form `pipeline:<projectId>:<stage>:<journeyId>` so it cannot duplicate an existing one.",
|
|
2679
|
+
"The worker independently reconciles the same pipeline from the artifacts on disk, so a follow-up you skip is still picked up. Never invent a stage that the evidence does not support just to keep the pipeline moving, and never mark work completed that you did not actually finish — a blocked task with concrete next steps is more useful than a false completion.",
|
|
2680
|
+
].join("\n"),
|
|
2681
|
+
}],
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
if (uri.startsWith("bugmole://spec/")) {
|
|
2685
|
+
const filePath = uri.replace("bugmole://spec/", "");
|
|
2686
|
+
const fullPath = path.join(specDir, filePath);
|
|
2687
|
+
|
|
2688
|
+
if (!fs.existsSync(fullPath)) {
|
|
2689
|
+
throw new Error(`Resource not found: ${uri}`);
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
2693
|
+
const mimeType =
|
|
2694
|
+
filePath.endsWith(".yaml") || filePath.endsWith(".yml")
|
|
2695
|
+
? "application/x-yaml"
|
|
2696
|
+
: "application/json";
|
|
2697
|
+
|
|
2698
|
+
return {
|
|
2699
|
+
contents: [
|
|
2700
|
+
{
|
|
2701
|
+
uri,
|
|
2702
|
+
mimeType,
|
|
2703
|
+
text: content,
|
|
2704
|
+
},
|
|
2705
|
+
],
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
throw new Error(`Unknown resource URI: ${uri}`);
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
export async function getPrompt(params: any, cfg: any): Promise<any> {
|
|
2713
|
+
const { name, arguments: args } = params;
|
|
2714
|
+
const promptsDir = path.join(process.cwd(), "docs");
|
|
2715
|
+
|
|
2716
|
+
switch (name) {
|
|
2717
|
+
case "guided_testing_workflow":
|
|
2718
|
+
return {
|
|
2719
|
+
messages: [
|
|
2720
|
+
{
|
|
2721
|
+
role: "user",
|
|
2722
|
+
content: {
|
|
2723
|
+
type: "text",
|
|
2724
|
+
text: [
|
|
2725
|
+
"You are operating under Bugmole's guided testing workflow.",
|
|
2726
|
+
"First read the bugmole://guidance/agent-testing resource.",
|
|
2727
|
+
"Then inspect the repository's package/config files, application entrypoints, routes, and relevant spec files before choosing a journey.",
|
|
2728
|
+
"Use bugmole_explore for source/application discovery when a runnable base URL exists. If you have an authenticated browser session, inspect the owner dashboard there and persist concrete observations with bugmole_record_discovery.",
|
|
2729
|
+
"After every meaningful phase, call bugmole_report_progress, read the current journey chart, and call bugmole_journey_next.",
|
|
2730
|
+
"Perform the returned flow task, record its concrete screen/transition evidence, and repeat the chart check until it returns exhausted or blocked.",
|
|
2731
|
+
"Before bugmole_plan, verify that the journey graph has real nodes/actions, no actionable next task remains, and the actor exists in roles.yaml.",
|
|
2732
|
+
"If discovery is incomplete, return a blocked status with concrete next_steps instead of creating a generic login-only plan.",
|
|
2733
|
+
"After planning, read the generated plan resource, validate its steps against the discovered evidence, and only then hand it to the dashboard for execution.",
|
|
2734
|
+
].join("\n"),
|
|
2735
|
+
},
|
|
2736
|
+
},
|
|
2737
|
+
],
|
|
2738
|
+
};
|
|
2739
|
+
case "app_ui_audit":
|
|
2740
|
+
return {
|
|
2741
|
+
messages: [
|
|
2742
|
+
{
|
|
2743
|
+
role: "user",
|
|
2744
|
+
content: {
|
|
2745
|
+
type: "text",
|
|
2746
|
+
text: [
|
|
2747
|
+
"You are performing Bugmole's application UI audit task.",
|
|
2748
|
+
"First select the requested journey and reproduce its user flow in the running product.",
|
|
2749
|
+
"Before auditing, call `bugmole_journey_next` and perform every actionable task, recording each screen, transition, and terminal goal completion. Do not audit a generic login or first screen.",
|
|
2750
|
+
"If the flow is blocked, check the returned blocker fingerprint and remembered resolution. Ask the user for clarification when needed, record the answer with `bugmole_resolve_blocker`, and retry before proceeding.",
|
|
2751
|
+
"Only call `bugmole_write_ui_audit` when the flow is exhausted or explicitly blocked. Include journeyId, journeyRevision, exploredNodeIds, exploredEdgeIds, and the matching flowStatus.",
|
|
2752
|
+
"Then audit every user-facing button, link, row, card, and other interactive control on each screen in the flow.",
|
|
2753
|
+
"For every control, report its visible label or action, status (Implemented, Partially implemented, or Not implemented), destination/callback/handler, runtime or source-code evidence, and missing backend, persistence, validation, or integration behavior.",
|
|
2754
|
+
"Implemented means the intended action works end-to-end. Partially implemented means the UI responds but some persistence, backend, validation, or integration is incomplete. Not implemented means the control is dead, has a no-op handler, only shows a placeholder/mock message, or has no working destination.",
|
|
2755
|
+
"Include navigation controls, tabs, forms, selectable cards, external links, CTAs, menu rows, and controls that visually appear interactive. Do not count static text as a link unless it has a real handler.",
|
|
2756
|
+
"Verify findings at runtime where possible, then inspect source code for hidden or unreachable controls. Ignore unreachable legacy branches unless they affect the active flow.",
|
|
2757
|
+
"Report status totals, complete screen-by-screen findings, evidence for each finding, highest-priority missing implementations, and blocked verification steps with reasons.",
|
|
2758
|
+
"The canonical deliverable is JSON, not markdown. Read bugmole://spec/app-ui-audit.schema.json, construct one object matching it, and call bugmole_write_ui_audit. The tool writes the project-scoped JSON file and returns its artifact key.",
|
|
2759
|
+
"A visual artifact or canvas may supplement a large audit, but it never replaces the required JSON file.",
|
|
2760
|
+
].join("\n"),
|
|
2761
|
+
},
|
|
2762
|
+
},
|
|
2763
|
+
],
|
|
2764
|
+
};
|
|
2765
|
+
case "ui_parity_audit":
|
|
2766
|
+
return {
|
|
2767
|
+
messages: [
|
|
2768
|
+
{
|
|
2769
|
+
role: "user",
|
|
2770
|
+
content: {
|
|
2771
|
+
type: "text",
|
|
2772
|
+
text: [
|
|
2773
|
+
"You are performing Bugmole's UI parity audit task.",
|
|
2774
|
+
"The user has supplied a reference experience and the current application has already been implemented. Do not start by inventing a reference and do not duplicate the control-by-control app UI audit.",
|
|
2775
|
+
"Read bugmole://guidance/ui-parity-audit and bugmole://spec/ui-parity-audit.schema.json first.",
|
|
2776
|
+
"Create a flow matrix covering every reachable reference screen, navigation path, interaction, state transition, authentication state, loading state, error state, and backend-dependent action.",
|
|
2777
|
+
"Systematically explore the reference and current app, capture before/after screenshots for meaningful actions, map reference actions to current routes/components/API callers, and verify guest, authenticated, loading, empty, error, logout, deep-link, callback, persistence, and API-error states.",
|
|
2778
|
+
"Capture reference screens with Playwright and current app screens with `adb exec-out screencap -p`. Normalize both to 390x844 (or another explicitly recorded viewport), then run `python scripts/ui-parity-diff.py` with Pillow to compute thresholded `ImageChops.difference()` pixels and a heatmap. A 0% difference score means identical pixels; higher percentages mean more visual difference.",
|
|
2779
|
+
"Record diff.json, normalized screenshots, and heatmap paths. Separate visual, behavioral, backend, and authentication parity.",
|
|
2780
|
+
"Fix confirmed issues in place when authorized, rebuild, replay changed flows, recapture screenshots, rerun comparisons, and inspect runtime/auth/network/API logs.",
|
|
2781
|
+
"The required final deliverable is a JSON object matching the parity schema. Include reference/current sources, flowMatrix, comparisons with differencePercent/changedPixels/totalPixels/heatmap, separate parity scores, issues (each tagged with the screenId it was found on, so the dashboard can group the bug list by step), artifact paths, blocked verification, and a concise fixPrompt. Call bugmole_write_ui_parity_audit so the dashboard can consume the JSON artifact.",
|
|
2782
|
+
].join("\n"),
|
|
2783
|
+
},
|
|
2784
|
+
},
|
|
2785
|
+
],
|
|
2786
|
+
};
|
|
2787
|
+
case "ui_reverse_engineering":
|
|
2788
|
+
return {
|
|
2789
|
+
messages: [{
|
|
2790
|
+
role: "user",
|
|
2791
|
+
content: {
|
|
2792
|
+
type: "text",
|
|
2793
|
+
text: [
|
|
2794
|
+
"You are Bugmole's UI Reverse Engineering Coordinator.",
|
|
2795
|
+
"Read bugmole://guidance/ui-reverse-engineering and bugmole://spec/ui-reverse-engineering.schema.json first.",
|
|
2796
|
+
"Use bugmole_ui_re_start for the supplied image, Figma export, PDF, URL, HTML, or existing-app input, then advance the same job through measure, dna, atoms, and complete stages.",
|
|
2797
|
+
"Measure before inferring. Treat Design DNA as the project genome and reuse its tokens before introducing primitives. Record provenance and confidence for every decision.",
|
|
2798
|
+
"Build and validate Atomic Design output inside-out: atoms, molecules, organisms, templates, then pages. Treat atoms and molecules as the reusable component playbook. The atomic stage scans the project for .storybook/main.* and CSF *.stories.* files, persists a Storybook component catalog, and maps each catalog entry into the atomic graph; use the discovered stories as the source of truth instead of inventing components.",
|
|
2799
|
+
"When a mockup is supplied as a URL, browse to the URL and capture the rendered reference before extracting regions. Attach reference captures to the journey steps; if the reference is archived or unrelated, capture the current app instead, and report blocked capture attempts explicitly.",
|
|
2800
|
+
"For existing applications, map reference regions to routes/components and use the parity workflow for current captures. Report blocked adapters and low-confidence findings explicitly.",
|
|
2801
|
+
].join("\n"),
|
|
2802
|
+
},
|
|
2803
|
+
}],
|
|
2804
|
+
};
|
|
2805
|
+
case "agent_task_execution":
|
|
2806
|
+
return {
|
|
2807
|
+
messages: [{
|
|
2808
|
+
role: "user",
|
|
2809
|
+
content: {
|
|
2810
|
+
type: "text",
|
|
2811
|
+
text: [
|
|
2812
|
+
"You are continuing a dashboard-created Bugmole agent task.",
|
|
2813
|
+
"Read bugmole://guidance/agent-task-queue, then call bugmole_task_next with a stable workerId.",
|
|
2814
|
+
"Implement the claimed task and report progress. For UI tasks, rerun parity after the fix and do not complete without a fresh parity audit id and match score.",
|
|
2815
|
+
"Call bugmole_task_update with completed or blocked status and concrete evidence.",
|
|
2816
|
+
].join("\n"),
|
|
2817
|
+
},
|
|
2818
|
+
}],
|
|
2819
|
+
};
|
|
2820
|
+
case "file_authoring":
|
|
2821
|
+
const authoringPath = path.join(promptsDir, "file-authoring-template.md");
|
|
2822
|
+
if (fs.existsSync(authoringPath)) {
|
|
2823
|
+
let template = fs.readFileSync(authoringPath, "utf-8");
|
|
2824
|
+
// Replace template variables
|
|
2825
|
+
if (args.filePath) template = template.replace(/<FILE_PATH>/g, args.filePath);
|
|
2826
|
+
if (args.filePurpose) template = template.replace(/<FILE_PURPOSE>/g, args.filePurpose);
|
|
2827
|
+
return {
|
|
2828
|
+
messages: [{ role: "user", content: { type: "text", text: template } }],
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
throw new Error("File authoring template not found");
|
|
2832
|
+
|
|
2833
|
+
case "diff_proposal":
|
|
2834
|
+
const diffPath = path.join(promptsDir, "diff-proposal-template.md");
|
|
2835
|
+
if (fs.existsSync(diffPath)) {
|
|
2836
|
+
let template = fs.readFileSync(diffPath, "utf-8");
|
|
2837
|
+
if (args.filePath) template = template.replace(/<FILE_PATH>/g, args.filePath);
|
|
2838
|
+
if (args.filePurpose) template = template.replace(/<FILE_PURPOSE>/g, args.filePurpose);
|
|
2839
|
+
return {
|
|
2840
|
+
messages: [{ role: "user", content: { type: "text", text: template } }],
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2843
|
+
throw new Error("Diff proposal template not found");
|
|
2844
|
+
|
|
2845
|
+
case "review_approval":
|
|
2846
|
+
const reviewPath = path.join(promptsDir, "review-approval-template.md");
|
|
2847
|
+
if (fs.existsSync(reviewPath)) {
|
|
2848
|
+
let template = fs.readFileSync(reviewPath, "utf-8");
|
|
2849
|
+
if (args.filePath) template = template.replace(/<FILE_PATH>/g, args.filePath);
|
|
2850
|
+
if (args.filePurpose) template = template.replace(/<FILE_PURPOSE>/g, args.filePurpose);
|
|
2851
|
+
return {
|
|
2852
|
+
messages: [{ role: "user", content: { type: "text", text: template } }],
|
|
2853
|
+
};
|
|
2854
|
+
}
|
|
2855
|
+
throw new Error("Review approval template not found");
|
|
2856
|
+
|
|
2857
|
+
default:
|
|
2858
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
export function readSpecFile(filePath: string, cfg: any): any {
|
|
2863
|
+
const specDir = cfg.runtime.spec_dir || "./spec";
|
|
2864
|
+
|
|
2865
|
+
// Try multiple path resolution strategies
|
|
2866
|
+
const possiblePaths: string[] = [];
|
|
2867
|
+
|
|
2868
|
+
// If filePath is already absolute, try it first
|
|
2869
|
+
if (path.isAbsolute(filePath)) {
|
|
2870
|
+
possiblePaths.push(filePath);
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
// Standard: spec_dir + filePath
|
|
2874
|
+
possiblePaths.push(path.join(specDir, filePath));
|
|
2875
|
+
|
|
2876
|
+
// Remove spec/ prefix if present (e.g., "spec/plans/file.yaml" -> "plans/file.yaml")
|
|
2877
|
+
if (filePath.startsWith("spec/")) {
|
|
2878
|
+
possiblePaths.push(path.join(specDir, filePath.replace(/^spec\//, "")));
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
// Try with resolved absolute paths
|
|
2882
|
+
possiblePaths.push(path.resolve(specDir, filePath));
|
|
2883
|
+
if (filePath.startsWith("spec/")) {
|
|
2884
|
+
possiblePaths.push(path.resolve(specDir, filePath.replace(/^spec\//, "")));
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
// Try from current working directory
|
|
2888
|
+
possiblePaths.push(path.join(process.cwd(), specDir, filePath));
|
|
2889
|
+
if (filePath.startsWith("spec/")) {
|
|
2890
|
+
possiblePaths.push(path.join(process.cwd(), specDir, filePath.replace(/^spec\//, "")));
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
// Try common project locations (if we're in ai-qa-framework but working on neko-coffee)
|
|
2894
|
+
// This is a fallback for when MCP server runs from framework but spec is in project
|
|
2895
|
+
const commonProjectDirs = [
|
|
2896
|
+
process.env.CURSOR_PROJECT_DIRECTORY,
|
|
2897
|
+
process.env.BUGMOLE_PROJECT_DIR,
|
|
2898
|
+
process.cwd(),
|
|
2899
|
+
].filter(Boolean);
|
|
2900
|
+
|
|
2901
|
+
for (const projectDir of commonProjectDirs) {
|
|
2902
|
+
if (projectDir) {
|
|
2903
|
+
const projectSpecDir = path.join(projectDir, "spec");
|
|
2904
|
+
possiblePaths.push(path.join(projectSpecDir, filePath));
|
|
2905
|
+
if (filePath.startsWith("spec/")) {
|
|
2906
|
+
possiblePaths.push(path.join(projectSpecDir, filePath.replace(/^spec\//, "")));
|
|
2907
|
+
}
|
|
2908
|
+
// Also try without spec/ prefix
|
|
2909
|
+
if (!filePath.startsWith("spec/")) {
|
|
2910
|
+
possiblePaths.push(path.join(projectSpecDir, filePath));
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
// Additional fallback: if we're in ai-qa-framework, try to find neko-coffee or other common project names
|
|
2916
|
+
// This is a heuristic - look for parent directories and sibling directories that might contain the project
|
|
2917
|
+
if (process.cwd().includes("ai-qa-framework")) {
|
|
2918
|
+
const parentDir = path.dirname(process.cwd());
|
|
2919
|
+
const commonProjectNames = ["neko-coffee", "kive", "neko-coffee-pos"];
|
|
2920
|
+
|
|
2921
|
+
// Try direct siblings first
|
|
2922
|
+
for (const projectName of commonProjectNames) {
|
|
2923
|
+
const potentialProjectDir = path.join(parentDir, projectName);
|
|
2924
|
+
if (fs.existsSync(potentialProjectDir)) {
|
|
2925
|
+
const projectSpecDir = path.join(potentialProjectDir, "spec");
|
|
2926
|
+
possiblePaths.push(path.join(projectSpecDir, filePath));
|
|
2927
|
+
if (filePath.startsWith("spec/")) {
|
|
2928
|
+
possiblePaths.push(path.join(projectSpecDir, filePath.replace(/^spec\//, "")));
|
|
2929
|
+
}
|
|
2930
|
+
if (!filePath.startsWith("spec/")) {
|
|
2931
|
+
possiblePaths.push(path.join(projectSpecDir, filePath));
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
// Also try nested paths like Projects/kive/neko-coffee
|
|
2937
|
+
for (const projectName of commonProjectNames) {
|
|
2938
|
+
// Try kive/neko-coffee structure
|
|
2939
|
+
const kiveDir = path.join(parentDir, "kive");
|
|
2940
|
+
if (fs.existsSync(kiveDir)) {
|
|
2941
|
+
const potentialProjectDir = path.join(kiveDir, projectName);
|
|
2942
|
+
if (fs.existsSync(potentialProjectDir)) {
|
|
2943
|
+
const projectSpecDir = path.join(potentialProjectDir, "spec");
|
|
2944
|
+
possiblePaths.push(path.join(projectSpecDir, filePath));
|
|
2945
|
+
if (filePath.startsWith("spec/")) {
|
|
2946
|
+
possiblePaths.push(path.join(projectSpecDir, filePath.replace(/^spec\//, "")));
|
|
2947
|
+
}
|
|
2948
|
+
if (!filePath.startsWith("spec/")) {
|
|
2949
|
+
possiblePaths.push(path.join(projectSpecDir, filePath));
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
// Last resort: search common project locations more broadly
|
|
2956
|
+
// Look for any directory containing 'neko-coffee' or 'kive' in the Projects folder
|
|
2957
|
+
const projectsDir = parentDir;
|
|
2958
|
+
if (fs.existsSync(projectsDir)) {
|
|
2959
|
+
try {
|
|
2960
|
+
const entries = fs.readdirSync(projectsDir, { withFileTypes: true });
|
|
2961
|
+
for (const entry of entries) {
|
|
2962
|
+
if (entry.isDirectory()) {
|
|
2963
|
+
const dirPath = path.join(projectsDir, entry.name);
|
|
2964
|
+
// Check if this directory or its children might be the project
|
|
2965
|
+
if (entry.name === "kive" || entry.name.includes("neko")) {
|
|
2966
|
+
// Check for neko-coffee inside
|
|
2967
|
+
try {
|
|
2968
|
+
const subEntries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
2969
|
+
for (const subEntry of subEntries) {
|
|
2970
|
+
if (
|
|
2971
|
+
subEntry.isDirectory() &&
|
|
2972
|
+
(subEntry.name.includes("neko") || subEntry.name.includes("coffee"))
|
|
2973
|
+
) {
|
|
2974
|
+
const potentialProjectDir = path.join(dirPath, subEntry.name);
|
|
2975
|
+
const projectSpecDir = path.join(potentialProjectDir, "spec");
|
|
2976
|
+
if (fs.existsSync(projectSpecDir)) {
|
|
2977
|
+
possiblePaths.push(path.join(projectSpecDir, filePath));
|
|
2978
|
+
if (filePath.startsWith("spec/")) {
|
|
2979
|
+
possiblePaths.push(
|
|
2980
|
+
path.join(projectSpecDir, filePath.replace(/^spec\//, "")),
|
|
2981
|
+
);
|
|
2982
|
+
}
|
|
2983
|
+
if (!filePath.startsWith("spec/")) {
|
|
2984
|
+
possiblePaths.push(path.join(projectSpecDir, filePath));
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
} catch {
|
|
2990
|
+
// Ignore errors reading subdirectories
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
} catch {
|
|
2996
|
+
// Ignore errors reading Projects directory
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
// Remove duplicates
|
|
3002
|
+
const uniquePaths = Array.from(new Set(possiblePaths));
|
|
3003
|
+
|
|
3004
|
+
let fullPath: string | null = null;
|
|
3005
|
+
for (const tryPath of uniquePaths) {
|
|
3006
|
+
try {
|
|
3007
|
+
if (fs.existsSync(tryPath)) {
|
|
3008
|
+
fullPath = tryPath;
|
|
3009
|
+
break;
|
|
3010
|
+
}
|
|
3011
|
+
} catch {
|
|
3012
|
+
// Ignore errors checking paths
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
if (!fullPath) {
|
|
3017
|
+
// Provide helpful error message with debug info
|
|
3018
|
+
const debugInfo = {
|
|
3019
|
+
filePath,
|
|
3020
|
+
specDir,
|
|
3021
|
+
cwd: process.cwd(),
|
|
3022
|
+
specDirResolved: path.resolve(specDir),
|
|
3023
|
+
specDirExists: fs.existsSync(specDir),
|
|
3024
|
+
triedPaths: uniquePaths.slice(0, 5), // Show first 5 for brevity
|
|
3025
|
+
env: {
|
|
3026
|
+
CURSOR_PROJECT_DIRECTORY: process.env.CURSOR_PROJECT_DIRECTORY,
|
|
3027
|
+
BUGMOLE_PROJECT_DIR: process.env.BUGMOLE_PROJECT_DIR,
|
|
3028
|
+
},
|
|
3029
|
+
};
|
|
3030
|
+
throw new Error(
|
|
3031
|
+
`Spec file not found: ${filePath}\n` +
|
|
3032
|
+
`Tried ${uniquePaths.length} paths. First few: ${uniquePaths.slice(0, 3).join(", ")}\n` +
|
|
3033
|
+
`Current working directory: ${process.cwd()}\n` +
|
|
3034
|
+
`Spec dir from config: ${specDir} (resolved: ${path.resolve(specDir)})\n` +
|
|
3035
|
+
`Hint: Make sure the spec files exist in the project directory, not the framework directory.`,
|
|
3036
|
+
);
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
3040
|
+
return {
|
|
3041
|
+
content,
|
|
3042
|
+
path: filePath,
|
|
3043
|
+
fullPath: fullPath, // Include resolved path for debugging
|
|
3044
|
+
size: content.length,
|
|
3045
|
+
};
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
export function writeSpecFile(filePath: string, content: string, cfg: any): any {
|
|
3049
|
+
const specDir = cfg.runtime.spec_dir || "./spec";
|
|
3050
|
+
const fullPath = path.join(specDir, filePath);
|
|
3051
|
+
|
|
3052
|
+
// Ensure directory exists
|
|
3053
|
+
const dir = path.dirname(fullPath);
|
|
3054
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
3055
|
+
|
|
3056
|
+
// Write file
|
|
3057
|
+
fs.writeFileSync(fullPath, content, "utf-8");
|
|
3058
|
+
|
|
3059
|
+
return {
|
|
3060
|
+
success: true,
|
|
3061
|
+
path: filePath,
|
|
3062
|
+
size: content.length,
|
|
3063
|
+
};
|
|
3064
|
+
}
|